From 075dfecd12b1126c8e0905d751f6ff195b3507f6 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 17 Mar 2026 10:31:11 -0400 Subject: [PATCH] refactor: consolidate experimental chats API types (#23143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Consolidates three areas of type duplication in the experimental chats API: ### 1. Merge archive/unarchive into `PATCH /{chat}` - **Before:** `POST /{chat}/archive` + `POST /{chat}/unarchive` (two endpoints, two handlers with mirrored logic) - **After:** `PATCH /{chat}` accepting `{ "archived": true/false }` via `UpdateChatRequest` - Removes one endpoint and ~30 lines of duplicated handler code ### 2. Collapse identical request/response prompt types - `ChatSystemPromptResponse` + `UpdateChatSystemPromptRequest` → `ChatSystemPrompt` - `UserChatCustomPromptResponse` + `UpdateUserChatCustomPromptRequest` → `UserChatCustomPrompt` - These pairs were field-for-field identical (single string field) ### 3. Merge duplicate reasoning options types - `ChatModelOpenRouterReasoningOptions` + `ChatModelVercelReasoningOptions` → `ChatModelReasoningOptions` - Same 4 fields, same types — only field ordering and enum value sets differed - Unified type uses the superset of enum values ### Files changed - `codersdk/chats.go` — SDK types and client methods - `coderd/chats.go` — Handler consolidation - `coderd/coderd.go` — Route change - `coderd/chats_test.go` — Test updates - `site/src/api/api.ts` — Frontend API client - `site/src/api/queries/chats.ts` — Query mutations - `site/src/api/queries/chats.test.ts` — Test mocks - `site/src/pages/AgentsPage/AgentsPage.tsx` — Call site - Generated files (`typesGenerated.ts`, `chatModelOptionsGenerated.json`) ### Testing - All Go tests pass (`TestArchiveChat`, `TestUnarchiveChat`, `TestChatSystemPrompt`) - All frontend tests pass (31/31 in `chats.test.ts`) --- .../chatd/chatprovider/chatprovider_test.go | 4 +- coderd/chats.go | 106 +++++++++--------- coderd/chats_test.go | 24 ++-- coderd/coderd.go | 3 +- codersdk/chats.go | 101 ++++++----------- site/src/api/api.ts | 49 ++++---- site/src/api/chatModelOptionsGenerated.json | 18 +-- site/src/api/queries/chats.test.ts | 7 +- site/src/api/queries/chats.ts | 4 +- site/src/api/typesGenerated.ts | 69 ++++-------- site/src/pages/AgentsPage/AgentsPage.tsx | 2 +- 11 files changed, 161 insertions(+), 226 deletions(-) diff --git a/coderd/chatd/chatprovider/chatprovider_test.go b/coderd/chatd/chatprovider/chatprovider_test.go index 57f5e1708b..8737be0ca7 100644 --- a/coderd/chatd/chatprovider/chatprovider_test.go +++ b/coderd/chatd/chatprovider/chatprovider_test.go @@ -82,7 +82,7 @@ func TestMergeMissingProviderOptions_OpenRouterNested(t *testing.T) { options := &codersdk.ChatModelProviderOptions{ OpenRouter: &codersdk.ChatModelOpenRouterProviderOptions{ - Reasoning: &codersdk.ChatModelOpenRouterReasoningOptions{ + Reasoning: &codersdk.ChatModelReasoningOptions{ Enabled: boolPtr(true), }, Provider: &codersdk.ChatModelOpenRouterProvider{ @@ -92,7 +92,7 @@ func TestMergeMissingProviderOptions_OpenRouterNested(t *testing.T) { } defaults := &codersdk.ChatModelProviderOptions{ OpenRouter: &codersdk.ChatModelOpenRouterProviderOptions{ - Reasoning: &codersdk.ChatModelOpenRouterReasoningOptions{ + Reasoning: &codersdk.ChatModelReasoningOptions{ Enabled: boolPtr(false), Exclude: boolPtr(true), MaxTokens: int64Ptr(123), diff --git a/coderd/chats.go b/coderd/chats.go index a7d0a09414..9fdaf7351a 100644 --- a/coderd/chats.go +++ b/coderd/chats.go @@ -1369,64 +1369,58 @@ func (api *API) watchChatDesktop(rw http.ResponseWriter, r *http.Request) { logger.Debug(ctx, "desktop Bicopy finished") } -// EXPERIMENTAL: this endpoint is experimental and is subject to change. -func (api *API) archiveChat(rw http.ResponseWriter, r *http.Request) { +// patchChat updates a chat resource. Currently supports toggling the +// archived state via the Archived field. +func (api *API) patchChat(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() chat := httpmw.ChatParam(r) - if chat.Archived { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Chat is already archived.", - }) + var req codersdk.UpdateChatRequest + if !httpapi.Read(ctx, rw, r, &req) { return } - var err error - // Use chatDaemon when available so it can notify - // active subscribers. Fall back to direct DB for the - // simple archive flag — no streaming state is involved. - if api.chatDaemon != nil { - err = api.chatDaemon.ArchiveChat(ctx, chat) - } else { - err = api.Database.ArchiveChatByID(ctx, chat.ID) - } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to archive chat.", - Detail: err.Error(), - }) - return - } + if req.Archived != nil { + archived := *req.Archived + if archived == chat.Archived { + state := "archived" + if !archived { + state = "not archived" + } + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Chat is already %s.", state), + }) + return + } - rw.WriteHeader(http.StatusNoContent) -} - -func (api *API) unarchiveChat(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - chat := httpmw.ChatParam(r) - - if !chat.Archived { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Chat is not archived.", - }) - return - } - - var err error - // Use chatDaemon when available so it can notify - // active subscribers. Fall back to direct DB for the - // simple unarchive flag — no streaming state is involved. - if api.chatDaemon != nil { - err = api.chatDaemon.UnarchiveChat(ctx, chat) - } else { - err = api.Database.UnarchiveChatByID(ctx, chat.ID) - } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to unarchive chat.", - Detail: err.Error(), - }) - return + var err error + // Use chatDaemon when available so it can notify active + // subscribers. Fall back to direct DB for the simple + // archive flag — no streaming state is involved. + if archived { + if api.chatDaemon != nil { + err = api.chatDaemon.ArchiveChat(ctx, chat) + } else { + err = api.Database.ArchiveChatByID(ctx, chat.ID) + } + } else { + if api.chatDaemon != nil { + err = api.chatDaemon.UnarchiveChat(ctx, chat) + } else { + err = api.Database.UnarchiveChatByID(ctx, chat.ID) + } + } + if err != nil { + action := "archive" + if !archived { + action = "unarchive" + } + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: fmt.Sprintf("Failed to %s chat.", action), + Detail: err.Error(), + }) + return + } } rw.WriteHeader(http.StatusNoContent) @@ -2525,14 +2519,14 @@ func (api *API) getChatSystemPrompt(rw http.ResponseWriter, r *http.Request) { }) return } - httpapi.Write(ctx, rw, http.StatusOK, codersdk.ChatSystemPromptResponse{ + httpapi.Write(ctx, rw, http.StatusOK, codersdk.ChatSystemPrompt{ SystemPrompt: prompt, }) } func (api *API) putChatSystemPrompt(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - var req codersdk.UpdateChatSystemPromptRequest + var req codersdk.ChatSystemPrompt if !httpapi.Read(ctx, rw, r, &req) { return } @@ -2582,7 +2576,7 @@ func (api *API) getUserChatCustomPrompt(rw http.ResponseWriter, r *http.Request) customPrompt = "" } - httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserChatCustomPromptResponse{ + httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserChatCustomPrompt{ CustomPrompt: customPrompt, }) } @@ -2594,7 +2588,7 @@ func (api *API) putUserChatCustomPrompt(rw http.ResponseWriter, r *http.Request) apiKey = httpmw.APIKey(r) ) - var params codersdk.UpdateUserChatCustomPromptRequest + var params codersdk.UserChatCustomPrompt if !httpapi.Read(ctx, rw, r, ¶ms) { return } @@ -2621,7 +2615,7 @@ func (api *API) putUserChatCustomPrompt(rw http.ResponseWriter, r *http.Request) return } - httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserChatCustomPromptResponse{ + httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserChatCustomPrompt{ CustomPrompt: updatedConfig.Value, }) } diff --git a/coderd/chats_test.go b/coderd/chats_test.go index 7ca7c93cd6..5c6ada9e49 100644 --- a/coderd/chats_test.go +++ b/coderd/chats_test.go @@ -29,6 +29,7 @@ import ( "github.com/coder/coder/v2/coderd/externalauth" coderdpubsub "github.com/coder/coder/v2/coderd/pubsub" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" "github.com/coder/websocket" @@ -1687,7 +1688,7 @@ func TestArchiveChat(t *testing.T) { require.NoError(t, err) require.Len(t, chatsBeforeArchive, 2) - err = client.ArchiveChat(ctx, chatToArchive.ID) + err = client.UpdateChat(ctx, chatToArchive.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)}) require.NoError(t, err) // Default (no filter) returns only non-archived chats. @@ -1721,7 +1722,7 @@ func TestArchiveChat(t *testing.T) { client := newChatClient(t) _ = coderdtest.CreateFirstUser(t, client) - err := client.ArchiveChat(ctx, uuid.New()) + err := client.UpdateChat(ctx, uuid.New(), codersdk.UpdateChatRequest{Archived: ptr.Ref(true)}) requireSDKError(t, err, http.StatusNotFound) }) @@ -1764,7 +1765,7 @@ func TestArchiveChat(t *testing.T) { require.NoError(t, err) // Archive the parent via the API. - err = client.ArchiveChat(ctx, parentChat.ID) + err = client.UpdateChat(ctx, parentChat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)}) require.NoError(t, err) // archived:false should exclude the entire archived family. @@ -1811,7 +1812,7 @@ func TestUnarchiveChat(t *testing.T) { require.NoError(t, err) // Archive the chat first. - err = client.ArchiveChat(ctx, chat.ID) + err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)}) require.NoError(t, err) // Verify it's archived. @@ -1822,7 +1823,7 @@ func TestUnarchiveChat(t *testing.T) { require.Len(t, archivedChats, 1) require.True(t, archivedChats[0].Archived) // Unarchive the chat. - err = client.UnarchiveChat(ctx, chat.ID) + err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(false)}) require.NoError(t, err) // Verify it's no longer archived. @@ -1861,10 +1862,9 @@ func TestUnarchiveChat(t *testing.T) { require.NoError(t, err) // Trying to unarchive a non-archived chat should fail. - err = client.UnarchiveChat(ctx, chat.ID) + err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(false)}) requireSDKError(t, err, http.StatusBadRequest) }) - t.Run("NotFound", func(t *testing.T) { t.Parallel() @@ -1872,7 +1872,7 @@ func TestUnarchiveChat(t *testing.T) { client := newChatClient(t) _ = coderdtest.CreateFirstUser(t, client) - err := client.UnarchiveChat(ctx, uuid.New()) + err := client.UpdateChat(ctx, uuid.New(), codersdk.UpdateChatRequest{Archived: ptr.Ref(false)}) requireSDKError(t, err, http.StatusNotFound) }) } @@ -4512,7 +4512,7 @@ func TestChatSystemPrompt(t *testing.T) { t.Run("AdminCanSet", func(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) - err := adminClient.UpdateChatSystemPrompt(ctx, codersdk.UpdateChatSystemPromptRequest{ + err := adminClient.UpdateChatSystemPrompt(ctx, codersdk.ChatSystemPrompt{ SystemPrompt: "You are a helpful coding assistant.", }) require.NoError(t, err) @@ -4526,7 +4526,7 @@ func TestChatSystemPrompt(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) // Unset by sending an empty string. - err := adminClient.UpdateChatSystemPrompt(ctx, codersdk.UpdateChatSystemPromptRequest{ + err := adminClient.UpdateChatSystemPrompt(ctx, codersdk.ChatSystemPrompt{ SystemPrompt: "", }) require.NoError(t, err) @@ -4539,7 +4539,7 @@ func TestChatSystemPrompt(t *testing.T) { t.Run("NonAdminFails", func(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) - err := memberClient.UpdateChatSystemPrompt(ctx, codersdk.UpdateChatSystemPromptRequest{ + err := memberClient.UpdateChatSystemPrompt(ctx, codersdk.ChatSystemPrompt{ SystemPrompt: "This should fail.", }) requireSDKError(t, err, http.StatusNotFound) @@ -4560,7 +4560,7 @@ func TestChatSystemPrompt(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) tooLong := strings.Repeat("a", 131073) - err := adminClient.UpdateChatSystemPrompt(ctx, codersdk.UpdateChatSystemPromptRequest{ + err := adminClient.UpdateChatSystemPrompt(ctx, codersdk.ChatSystemPrompt{ SystemPrompt: tooLong, }) sdkErr := requireSDKError(t, err, http.StatusBadRequest) diff --git a/coderd/coderd.go b/coderd/coderd.go index 35f902207f..f1e8622a4b 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1196,8 +1196,7 @@ func New(options *Options) *API { r.Get("/", api.getChat) r.Get("/git/watch", api.watchChatGit) r.Get("/desktop", api.watchChatDesktop) - r.Post("/archive", api.archiveChat) - r.Post("/unarchive", api.unarchiveChat) + r.Patch("/", api.patchChat) r.Get("/messages", api.getChatMessages) r.Post("/messages", api.postChatMessages) r.Patch("/messages/{message}", api.patchChatMessage) diff --git a/codersdk/chats.go b/codersdk/chats.go index ef0cc4378a..2cec7186b7 100644 --- a/codersdk/chats.go +++ b/codersdk/chats.go @@ -245,7 +245,8 @@ type CreateChatRequest struct { // UpdateChatRequest is the request to update a chat. type UpdateChatRequest struct { - Title string `json:"title"` + Title *string `json:"title,omitempty"` + Archived *bool `json:"archived,omitempty"` } // CreateChatMessageRequest is the request to add a message to a chat. @@ -307,25 +308,15 @@ type ChatModelsResponse struct { Providers []ChatModelProvider `json:"providers"` } -// ChatSystemPromptResponse is the response for getting the chat system prompt. -type ChatSystemPromptResponse struct { +// ChatSystemPrompt is the request and response body for the chat +// system prompt configuration endpoint. +type ChatSystemPrompt struct { SystemPrompt string `json:"system_prompt"` } -// UpdateChatSystemPromptRequest is the request to update the chat system prompt. -type UpdateChatSystemPromptRequest struct { - SystemPrompt string `json:"system_prompt"` -} - -// UserChatCustomPromptResponse is the response for getting a user's -// custom chat prompt. -type UserChatCustomPromptResponse struct { - CustomPrompt string `json:"custom_prompt"` -} - -// UpdateUserChatCustomPromptRequest is the request to update a user's -// custom chat prompt. -type UpdateUserChatCustomPromptRequest struct { +// UserChatCustomPrompt is the request and response body for the +// user chat custom prompt configuration endpoint. +type UserChatCustomPrompt struct { CustomPrompt string `json:"custom_prompt"` } @@ -466,12 +457,13 @@ type ChatModelOpenAICompatProviderOptions struct { ReasoningEffort *string `json:"reasoning_effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"` } -// ChatModelOpenRouterReasoningOptions configures OpenRouter reasoning behavior. -type ChatModelOpenRouterReasoningOptions struct { +// ChatModelReasoningOptions configures reasoning behavior for model +// providers that support it. +type ChatModelReasoningOptions struct { Enabled *bool `json:"enabled,omitempty" description:"Whether reasoning is enabled"` Exclude *bool `json:"exclude,omitempty" description:"Whether to exclude reasoning content from the response"` MaxTokens *int64 `json:"max_tokens,omitempty" description:"Maximum number of tokens for reasoning output"` - Effort *string `json:"effort,omitempty" description:"Controls the level of reasoning effort" enum:"low,medium,high"` + Effort *string `json:"effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"` } // ChatModelOpenRouterProvider configures OpenRouter routing preferences. @@ -488,22 +480,14 @@ type ChatModelOpenRouterProvider struct { // ChatModelOpenRouterProviderOptions configures OpenRouter provider behavior. type ChatModelOpenRouterProviderOptions struct { - Reasoning *ChatModelOpenRouterReasoningOptions `json:"reasoning,omitempty" description:"Configuration for reasoning behavior"` - ExtraBody map[string]any `json:"extra_body,omitempty" description:"Additional fields to include in the request body" hidden:"true"` - IncludeUsage *bool `json:"include_usage,omitempty" description:"Whether to include token usage information in the response" hidden:"true"` - LogitBias map[string]int64 `json:"logit_bias,omitempty" description:"Token IDs mapped to bias values from -100 to 100" hidden:"true"` - LogProbs *bool `json:"log_probs,omitempty" description:"Whether to return log probabilities of output tokens" hidden:"true"` - ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty" description:"Whether the model may make multiple tool calls in parallel"` - User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"` - Provider *ChatModelOpenRouterProvider `json:"provider,omitempty" description:"Routing preferences for provider selection" hidden:"true"` -} - -// ChatModelVercelReasoningOptions configures Vercel reasoning behavior. -type ChatModelVercelReasoningOptions struct { - Enabled *bool `json:"enabled,omitempty" description:"Whether reasoning is enabled"` - MaxTokens *int64 `json:"max_tokens,omitempty" description:"Maximum number of tokens for reasoning output"` - Effort *string `json:"effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"` - Exclude *bool `json:"exclude,omitempty" description:"Whether to exclude reasoning content from the response"` + Reasoning *ChatModelReasoningOptions `json:"reasoning,omitempty" description:"Configuration for reasoning behavior"` + ExtraBody map[string]any `json:"extra_body,omitempty" description:"Additional fields to include in the request body" hidden:"true"` + IncludeUsage *bool `json:"include_usage,omitempty" description:"Whether to include token usage information in the response" hidden:"true"` + LogitBias map[string]int64 `json:"logit_bias,omitempty" description:"Token IDs mapped to bias values from -100 to 100" hidden:"true"` + LogProbs *bool `json:"log_probs,omitempty" description:"Whether to return log probabilities of output tokens" hidden:"true"` + ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty" description:"Whether the model may make multiple tool calls in parallel"` + User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"` + Provider *ChatModelOpenRouterProvider `json:"provider,omitempty" description:"Routing preferences for provider selection" hidden:"true"` } // ChatModelVercelGatewayProviderOptions configures Vercel routing behavior. @@ -514,7 +498,7 @@ type ChatModelVercelGatewayProviderOptions struct { // ChatModelVercelProviderOptions configures Vercel provider behavior. type ChatModelVercelProviderOptions struct { - Reasoning *ChatModelVercelReasoningOptions `json:"reasoning,omitempty" description:"Configuration for reasoning behavior"` + Reasoning *ChatModelReasoningOptions `json:"reasoning,omitempty" description:"Configuration for reasoning behavior"` ProviderOptions *ChatModelVercelGatewayProviderOptions `json:"providerOptions,omitempty" description:"Gateway routing options for provider selection" hidden:"true"` User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"` LogitBias map[string]int64 `json:"logit_bias,omitempty" description:"Token IDs mapped to bias values from -100 to 100" hidden:"true"` @@ -1246,21 +1230,21 @@ func (c *Client) GetChatCostUsers(ctx context.Context, opts ChatCostUsersOptions } // GetChatSystemPrompt returns the deployment-wide chat system prompt. -func (c *Client) GetChatSystemPrompt(ctx context.Context) (ChatSystemPromptResponse, error) { +func (c *Client) GetChatSystemPrompt(ctx context.Context) (ChatSystemPrompt, error) { res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/system-prompt", nil) if err != nil { - return ChatSystemPromptResponse{}, err + return ChatSystemPrompt{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return ChatSystemPromptResponse{}, ReadBodyAsError(res) + return ChatSystemPrompt{}, ReadBodyAsError(res) } - var resp ChatSystemPromptResponse + var resp ChatSystemPrompt return resp, json.NewDecoder(res.Body).Decode(&resp) } // UpdateChatSystemPrompt updates the deployment-wide chat system prompt. -func (c *Client) UpdateChatSystemPrompt(ctx context.Context, req UpdateChatSystemPromptRequest) error { +func (c *Client) UpdateChatSystemPrompt(ctx context.Context, req ChatSystemPrompt) error { res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/system-prompt", req) if err != nil { return err @@ -1273,30 +1257,30 @@ func (c *Client) UpdateChatSystemPrompt(ctx context.Context, req UpdateChatSyste } // GetUserChatCustomPrompt fetches the user's custom chat prompt. -func (c *Client) GetUserChatCustomPrompt(ctx context.Context) (UserChatCustomPromptResponse, error) { +func (c *Client) GetUserChatCustomPrompt(ctx context.Context) (UserChatCustomPrompt, error) { res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/user-prompt", nil) if err != nil { - return UserChatCustomPromptResponse{}, err + return UserChatCustomPrompt{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return UserChatCustomPromptResponse{}, ReadBodyAsError(res) + return UserChatCustomPrompt{}, ReadBodyAsError(res) } - var resp UserChatCustomPromptResponse + var resp UserChatCustomPrompt return resp, json.NewDecoder(res.Body).Decode(&resp) } // UpdateUserChatCustomPrompt updates the user's custom chat prompt. -func (c *Client) UpdateUserChatCustomPrompt(ctx context.Context, req UpdateUserChatCustomPromptRequest) (UserChatCustomPromptResponse, error) { +func (c *Client) UpdateUserChatCustomPrompt(ctx context.Context, req UserChatCustomPrompt) (UserChatCustomPrompt, error) { res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/user-prompt", req) if err != nil { - return UserChatCustomPromptResponse{}, err + return UserChatCustomPrompt{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return UserChatCustomPromptResponse{}, ReadBodyAsError(res) + return UserChatCustomPrompt{}, ReadBodyAsError(res) } - var resp UserChatCustomPromptResponse + var resp UserChatCustomPrompt return resp, json.NewDecoder(res.Body).Decode(&resp) } @@ -1499,20 +1483,9 @@ func (c *Client) GetChatMessages(ctx context.Context, chatID uuid.UUID, opts *Ch return resp, json.NewDecoder(res.Body).Decode(&resp) } -func (c *Client) ArchiveChat(ctx context.Context, chatID uuid.UUID) error { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/archive", chatID), nil) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return ReadBodyAsError(res) - } - return nil -} - -func (c *Client) UnarchiveChat(ctx context.Context, chatID uuid.UUID) error { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/unarchive", chatID), nil) +// UpdateChat patches a chat resource. +func (c *Client) UpdateChat(ctx context.Context, chatID uuid.UUID, req UpdateChatRequest) error { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/chats/%s", chatID), req) if err != nil { return err } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 862ab67cd9..6e0144a435 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2985,12 +2985,11 @@ class ApiMethods { return response.data; }; - archiveChat = async (chatId: string): Promise => { - await this.axios.post(`/api/experimental/chats/${chatId}/archive`); - }; - - unarchiveChat = async (chatId: string): Promise => { - await this.axios.post(`/api/experimental/chats/${chatId}/unarchive`); + updateChat = async ( + chatId: string, + req: TypesGen.UpdateChatRequest, + ): Promise => { + await this.axios.patch(`/api/experimental/chats/${chatId}`, req); }; createChatMessage = async ( @@ -3067,37 +3066,33 @@ class ApiMethods { return response.data; }; - getChatSystemPrompt = - async (): Promise => { - const response = await this.axios.get( - "/api/experimental/chats/config/system-prompt", - ); - return response.data; - }; + getChatSystemPrompt = async (): Promise => { + const response = await this.axios.get( + "/api/experimental/chats/config/system-prompt", + ); + return response.data; + }; updateChatSystemPrompt = async ( - req: TypesGen.UpdateChatSystemPromptRequest, + req: TypesGen.ChatSystemPrompt, ): Promise => { await this.axios.put("/api/experimental/chats/config/system-prompt", req); }; getUserChatCustomPrompt = - async (): Promise => { - const response = - await this.axios.get( - "/api/experimental/chats/config/user-prompt", - ); + async (): Promise => { + const response = await this.axios.get( + "/api/experimental/chats/config/user-prompt", + ); return response.data; }; - updateUserChatCustomPrompt = async ( - req: TypesGen.UpdateUserChatCustomPromptRequest, - ): Promise => { - const response = - await this.axios.put( - "/api/experimental/chats/config/user-prompt", - req, - ); + req: TypesGen.UserChatCustomPrompt, + ): Promise => { + const response = await this.axios.put( + "/api/experimental/chats/config/user-prompt", + req, + ); return response.data; }; diff --git a/site/src/api/chatModelOptionsGenerated.json b/site/src/api/chatModelOptionsGenerated.json index 14d866ff8d..ccaa04a712 100644 --- a/site/src/api/chatModelOptionsGenerated.json +++ b/site/src/api/chatModelOptionsGenerated.json @@ -457,7 +457,7 @@ "type": "string", "description": "Controls the level of reasoning effort", "required": false, - "enum": ["low", "medium", "high"], + "enum": ["none", "minimal", "low", "medium", "high", "xhigh"], "input_type": "select" }, { @@ -534,6 +534,14 @@ "required": false, "input_type": "select" }, + { + "json_name": "reasoning.exclude", + "go_name": "Reasoning.Exclude", + "type": "boolean", + "description": "Whether to exclude reasoning content from the response", + "required": false, + "input_type": "select" + }, { "json_name": "reasoning.max_tokens", "go_name": "Reasoning.MaxTokens", @@ -551,14 +559,6 @@ "enum": ["none", "minimal", "low", "medium", "high", "xhigh"], "input_type": "select" }, - { - "json_name": "reasoning.exclude", - "go_name": "Reasoning.Exclude", - "type": "boolean", - "description": "Whether to exclude reasoning content from the response", - "required": false, - "input_type": "select" - }, { "json_name": "providerOptions", "go_name": "ProviderOptions", diff --git a/site/src/api/queries/chats.test.ts b/site/src/api/queries/chats.test.ts index 97b8da3a5c..c612a047a9 100644 --- a/site/src/api/queries/chats.test.ts +++ b/site/src/api/queries/chats.test.ts @@ -25,10 +25,9 @@ import { vi.mock("api/api", () => ({ API: { - archiveChat: vi.fn(), + updateChat: vi.fn(), createChat: vi.fn(), deleteChatQueuedMessage: vi.fn(), - unarchiveChat: vi.fn(), getChats: vi.fn(), getChatCostSummary: vi.fn(), getChatCostUsers: vi.fn(), @@ -207,7 +206,7 @@ describe("archiveChat optimistic update", () => { const initialChats = [makeChat(chatId), makeChat("chat-2")]; seedInfiniteChats(queryClient, initialChats); - vi.mocked(API.archiveChat).mockResolvedValue(); + vi.mocked(API.updateChat).mockResolvedValue(); const mutation = archiveChat(queryClient); await mutation.onMutate(chatId); @@ -225,7 +224,7 @@ describe("archiveChat optimistic update", () => { seedInfiniteChats(queryClient, [makeChat(chatId)]); queryClient.setQueryData(chatKey(chatId), makeChat(chatId)); - vi.mocked(API.archiveChat).mockResolvedValue(); + vi.mocked(API.updateChat).mockResolvedValue(); const mutation = archiveChat(queryClient); await mutation.onMutate(chatId); diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts index d6913cc2e0..5382f81092 100644 --- a/site/src/api/queries/chats.ts +++ b/site/src/api/queries/chats.ts @@ -175,7 +175,7 @@ export const chatMessagesForInfiniteScroll = (chatId: string) => ({ }); export const archiveChat = (queryClient: QueryClient) => ({ - mutationFn: (chatId: string) => API.archiveChat(chatId), + mutationFn: (chatId: string) => API.updateChat(chatId, { archived: true }), onMutate: async (chatId: string) => { await queryClient.cancelQueries({ queryKey: chatsKey, @@ -234,7 +234,7 @@ export const archiveChat = (queryClient: QueryClient) => ({ }); export const unarchiveChat = (queryClient: QueryClient) => ({ - mutationFn: (chatId: string) => API.unarchiveChat(chatId), + mutationFn: (chatId: string) => API.updateChat(chatId, { archived: false }), onMutate: async (chatId: string) => { await queryClient.cancelQueries({ queryKey: chatsKey, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 047a7ff833..7b2630af6c 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1538,7 +1538,7 @@ export interface ChatModelOpenRouterProvider { * ChatModelOpenRouterProviderOptions configures OpenRouter provider behavior. */ export interface ChatModelOpenRouterProviderOptions { - readonly reasoning?: ChatModelOpenRouterReasoningOptions; + readonly reasoning?: ChatModelReasoningOptions; // empty interface{} type, falling back to unknown readonly extra_body?: Record; readonly include_usage?: boolean; @@ -1549,17 +1549,6 @@ export interface ChatModelOpenRouterProviderOptions { readonly provider?: ChatModelOpenRouterProvider; } -// From codersdk/chats.go -/** - * ChatModelOpenRouterReasoningOptions configures OpenRouter reasoning behavior. - */ -export interface ChatModelOpenRouterReasoningOptions { - readonly enabled?: boolean; - readonly exclude?: boolean; - readonly max_tokens?: number; - readonly effort?: string; -} - // From codersdk/chats.go /** * ChatModelProvider represents provider availability and model results. @@ -1595,6 +1584,18 @@ export type ChatModelProviderUnavailableReason = export const ChatModelProviderUnavailableReasons: ChatModelProviderUnavailableReason[] = ["fetch_failed", "missing_api_key"]; +// From codersdk/chats.go +/** + * ChatModelReasoningOptions configures reasoning behavior for model + * providers that support it. + */ +export interface ChatModelReasoningOptions { + readonly enabled?: boolean; + readonly exclude?: boolean; + readonly max_tokens?: number; + readonly effort?: string; +} + // From codersdk/chats.go /** * ChatModelVercelGatewayProviderOptions configures Vercel routing behavior. @@ -1609,7 +1610,7 @@ export interface ChatModelVercelGatewayProviderOptions { * ChatModelVercelProviderOptions configures Vercel provider behavior. */ export interface ChatModelVercelProviderOptions { - readonly reasoning?: ChatModelVercelReasoningOptions; + readonly reasoning?: ChatModelReasoningOptions; readonly providerOptions?: ChatModelVercelGatewayProviderOptions; readonly user?: string; readonly logit_bias?: Record; @@ -1620,17 +1621,6 @@ export interface ChatModelVercelProviderOptions { readonly extra_body?: Record; } -// From codersdk/chats.go -/** - * ChatModelVercelReasoningOptions configures Vercel reasoning behavior. - */ -export interface ChatModelVercelReasoningOptions { - readonly enabled?: boolean; - readonly max_tokens?: number; - readonly effort?: string; - readonly exclude?: boolean; -} - // From codersdk/chats.go /** * ChatModelsResponse is the catalog returned from chat model discovery. @@ -1777,9 +1767,10 @@ export interface ChatStreamStatus { // From codersdk/chats.go /** - * ChatSystemPromptResponse is the response for getting the chat system prompt. + * ChatSystemPrompt is the request and response body for the chat + * system prompt configuration endpoint. */ -export interface ChatSystemPromptResponse { +export interface ChatSystemPrompt { readonly system_prompt: string; } @@ -6608,15 +6599,8 @@ export interface UpdateChatProviderConfigRequest { * UpdateChatRequest is the request to update a chat. */ export interface UpdateChatRequest { - readonly title: string; -} - -// From codersdk/chats.go -/** - * UpdateChatSystemPromptRequest is the request to update the chat system prompt. - */ -export interface UpdateChatSystemPromptRequest { - readonly system_prompt: string; + readonly title?: string; + readonly archived?: boolean; } // From codersdk/chats.go @@ -6795,15 +6779,6 @@ export interface UpdateUserAppearanceSettingsRequest { readonly terminal_font: TerminalFontName; } -// From codersdk/chats.go -/** - * UpdateUserChatCustomPromptRequest is the request to update a user's - * custom chat prompt. - */ -export interface UpdateUserChatCustomPromptRequest { - readonly custom_prompt: string; -} - // From codersdk/notifications.go export interface UpdateUserNotificationPreferences { readonly template_disabled_map: Record; @@ -7048,10 +7023,10 @@ export interface UserAppearanceSettings { // From codersdk/chats.go /** - * UserChatCustomPromptResponse is the response for getting a user's - * custom chat prompt. + * UserChatCustomPrompt is the request and response body for the + * user chat custom prompt configuration endpoint. */ -export interface UserChatCustomPromptResponse { +export interface UserChatCustomPrompt { readonly custom_prompt: string; } diff --git a/site/src/pages/AgentsPage/AgentsPage.tsx b/site/src/pages/AgentsPage/AgentsPage.tsx index 55f743ef76..b2676eeabb 100644 --- a/site/src/pages/AgentsPage/AgentsPage.tsx +++ b/site/src/pages/AgentsPage/AgentsPage.tsx @@ -148,7 +148,7 @@ const AgentsPage: FC = () => { chatId: string; workspaceId: string; }) => { - await API.archiveChat(chatId); + await API.updateChat(chatId, { archived: true }); await API.deleteWorkspace(workspaceId); return { chatId, workspaceId }; },