feat: mount chat API routes under /api/v2 (#28496)

## Stack context

This is the base of a 3-PR stack promoting the chat API from
`/api/experimental` to `/api/v2`: server compatibility mounts (this PR),
codersdk promotion (#28497), and frontend path updates (#28498).

## Summary

Double-mount the stable chat and MCP handlers under `/api/v2` while
retaining the existing experimental routes for the one-release
compatibility window decided in CODAGT-921. CODAGT-922 tracks removing
the compatibility mounts.

The shared route builders preserve existing authentication and
middleware behavior. Experiment-gated, debug, tombstone, and legacy
default-organization model routes remain experimental-only. Signed file
URLs, external OAuth callback URLs, and mixed-version replica relays
also remain on the experimental prefix during the transition.

Update Swagger and the generated API reference for the promoted routes,
including the workspace lookup and a runnable raw-body chat file upload
example. Retain internal endpoints outside the published reference,
share chat-file rate limits across both prefixes, enable CORS for the v2
MCP routes, and cover dual mounts plus exclusions with compatibility
tests. Remote dogfood UAT passed for the promoted chat, model, MCP, and
file flows.

> [!NOTE]
> Xum acted on Mike's behalf in this pull request.
<!-- xum-attribution: model=claude-fable-5 thinking=high -->
This commit is contained in:
Michael Suchacz
2026-08-26 16:17:06 +02:00
committed by GitHub
parent 0c0e605521
commit 351bb14032
27 changed files with 8686 additions and 4537 deletions
+3194 -1996
View File
File diff suppressed because it is too large Load Diff
+2884 -1798
View File
File diff suppressed because it is too large Load Diff
+274
View File
@@ -0,0 +1,274 @@
package coderd
import (
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/codersdk"
)
// chatFilesRateLimitMW returns the middleware enforcing FilesRateLimit
// on chat file routes. Both API prefixes mount the same instance, and
// the limiter keys on a prefix-stripped endpoint, so alternating
// prefixes cannot double the budget.
func (api *API) chatFilesRateLimitMW() func(http.Handler) http.Handler {
api.chatFilesRateLimitOnce.Do(func() {
api.chatFilesRateLimit = httpmw.RateLimitByAPICompatibilityEndpoint(api.FilesRateLimit, time.Minute)
})
return api.chatFilesRateLimit
}
// chatAPIPrefix identifies which API prefix a chat route mount serves.
type chatAPIPrefix int
const (
chatAPIPrefixV2 chatAPIPrefix = iota
chatAPIPrefixExperimental
)
// injectDefaultOrganizationParam lets the legacy default-organization
// routes reuse the organization-scoped handlers.
func injectDefaultOrganizationParam(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
chi.RouteContext(req.Context()).URLParams.Add("organization", codersdk.DefaultOrganization)
next.ServeHTTP(rw, req)
})
}
// registerChatAPIRoutes mounts the chat API surface on r, the root router
// of an API prefix. /api/v2 and /api/experimental serve the same promoted
// routes during the CODAGT-921 compatibility window. The experimental
// mount also serves the routes that were not promoted, while /api/v2
// reserves their path segments so they return 404 instead of falling into
// the {chat} wildcard.
// TODO(CODAGT-921): unmount from /api/experimental after the transition
// window (tracked in CODAGT-922).
func (api *API) registerChatAPIRoutes(r chi.Router, apiKeyMiddleware func(http.Handler) http.Handler, prefix chatAPIPrefix) {
experimental := prefix == chatAPIPrefixExperimental
// Signed URL tokens authenticate downloads, so the route stays
// outside the API key middleware.
r.Group(func(r chi.Router) {
r.Use(api.chatFilesRateLimitMW())
r.Get("/chats/files/{file}/download", api.downloadChatFile)
})
if experimental {
// Superseded by the organization-scoped models collection and
// deliberately not promoted. Keep until the frontend uses the
// organization-scoped routes.
r.Route("/chats/model-configs", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
injectDefaultOrganizationParam,
httpmw.ExtractOrganizationParam(api.Database),
)
r.Get("/", api.listDefaultOrganizationChatModelConfigs)
r.Post("/", api.createChatModelConfig)
})
}
r.Route("/chats", func(r chi.Router) {
r.Use(apiKeyMiddleware)
if experimental {
// Superseded by the organization-scoped models route and
// deliberately not promoted. Keep until the frontend uses
// the organization-scoped routes.
r.With(
injectDefaultOrganizationParam,
httpmw.ExtractOrganizationParam(api.Database),
).Get("/models", api.listChatModelConfigsByOrganization)
// TODO(cian): place under /api/experimental/chats/config
r.Route("/providers", func(r chi.Router) {
r.Get("/", api.listChatProviders)
r.Post("/", api.createChatProvider)
r.Route("/{providerConfig}", func(r chi.Router) {
r.Patch("/", api.updateChatProvider)
r.Delete("/", api.deleteChatProvider)
})
})
r.Route("/user-provider-configs", func(r chi.Router) {
r.Get("/", api.listUserChatProviderConfigs)
r.Route("/{providerConfig}", func(r chi.Router) {
r.Put("/", api.upsertUserChatProviderKey)
r.Delete("/", api.deleteUserChatProviderKey)
})
})
} else {
// These segments exist only under /api/experimental. Reserve
// them with empty subrouters so they return 404 instead of
// falling into the {chat} wildcard and failing UUID parsing
// with a 400.
// TODO(CODAGT-922): drop the reservations with the
// experimental mounts.
for _, segment := range []string{"/models", "/model-configs", "/providers", "/user-provider-configs"} {
r.Route(segment, func(r chi.Router) {
r.NotFound(func(rw http.ResponseWriter, _ *http.Request) {
httpapi.RouteNotFound(rw)
})
})
}
}
r.Get("/by-workspace", api.chatsByWorkspace)
r.Get("/", api.listChats)
r.Post("/", api.postChats)
r.Get("/watch", api.watchChats)
r.Route("/files", func(r chi.Router) {
r.Use(api.chatFilesRateLimitMW())
r.Post("/", api.postChatFile)
r.Post("/{file}/download-url", api.postChatFileDownloadURL)
r.Get("/{file}", api.chatFileByID)
})
r.Route("/config", func(r chi.Router) {
r.Get("/system-prompt", api.getChatSystemPrompt)
r.Put("/system-prompt", api.putChatSystemPrompt)
r.Get("/plan-mode-instructions", api.getChatPlanModeInstructions)
r.Put("/plan-mode-instructions", api.putChatPlanModeInstructions)
r.Get("/personal-model-overrides", api.getChatPersonalModelOverridesAdminSettings)
r.Put("/personal-model-overrides", api.putChatPersonalModelOverridesAdminSettings)
r.Get("/debug-logging", api.getChatDebugLogging)
r.Put("/debug-logging", api.putChatDebugLogging)
r.Get("/user-debug-logging", api.getUserChatDebugLogging)
r.Put("/user-debug-logging", api.putUserChatDebugLogging)
r.Get("/user-prompt", api.getUserChatCustomPrompt)
r.Put("/user-prompt", api.putUserChatCustomPrompt)
r.Get("/user-compaction-thresholds", api.getUserChatCompactionThresholds)
r.Put("/user-compaction-thresholds/{modelConfig}", api.putUserChatCompactionThreshold)
r.Delete("/user-compaction-thresholds/{modelConfig}", api.deleteUserChatCompactionThreshold)
r.Get("/workspace-ttl", api.getChatWorkspaceTTL)
r.Put("/workspace-ttl", api.putChatWorkspaceTTL)
r.Get("/retention-days", api.getChatRetentionDays)
r.Put("/retention-days", api.putChatRetentionDays)
r.Get("/debug-retention-days", api.getChatDebugRetentionDays)
r.Put("/debug-retention-days", api.putChatDebugRetentionDays)
r.Get("/auto-archive-days", api.getChatAutoArchiveDays)
r.Put("/auto-archive-days", api.putChatAutoArchiveDays)
if experimental {
r.Group(func(r chi.Router) {
r.Use(httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentChatVirtualDesktop))
r.Get("/computer-use-provider", api.getChatComputerUseProvider)
r.Put("/computer-use-provider", api.putChatComputerUseProvider)
})
r.Group(func(r chi.Router) {
r.Use(httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentChatAdvisor))
r.Get("/advisor", api.getChatAdvisorConfig)
r.Put("/advisor", api.putChatAdvisorConfig)
})
}
})
r.Route("/{chat}", func(r chi.Router) {
r.Use(httpmw.ExtractChatParam(api.Database))
r.Route("/acl", func(r chi.Router) {
r.Get("/", api.getChatACL)
r.Patch("/", api.patchChatACL)
})
r.Get("/", api.getChat)
r.Patch("/", api.patchChat)
r.Get("/cost", api.getChatCost)
r.Get("/messages", api.getChatMessages)
r.Post("/messages", api.postChatMessages)
r.Patch("/messages/{message}", api.patchChatMessage)
r.Get("/prompts", api.getChatUserPrompts)
r.Post("/interrupt", api.interruptChat)
r.Post("/compact", api.compactChat)
r.Post("/reconcile-invalid", api.reconcileInvalidChatState)
r.Post("/tool-results", api.postChatToolResults)
r.Post("/title/propose", api.proposeChatTitle)
r.Get("/diff", api.getChatDiffContents)
r.Put("/context", api.refreshChatContext)
r.Route("/queue/{queuedMessage}", func(r chi.Router) {
r.Delete("/", api.deleteChatQueuedMessage)
r.Post("/promote", api.promoteChatQueuedMessage)
})
r.Route("/stream", func(r chi.Router) {
r.Get("/", api.streamChat)
r.Get("/parts", api.streamChatParts)
r.Get("/git", api.watchChatGit)
if experimental {
r.Get("/desktop", api.watchChatDesktop)
}
})
if experimental {
r.Route("/debug", func(r chi.Router) {
r.Get("/runs", api.getChatDebugRuns)
r.Get("/runs/{debugRun}", api.getChatDebugRun)
})
}
})
})
}
// registerMCPServerOAuth2Routes mounts the user-scoped MCP server OAuth2
// routes shared by both API prefixes.
func (api *API) registerMCPServerOAuth2Routes(r chi.Router, prefix chatAPIPrefix) {
if prefix == chatAPIPrefixExperimental {
// Providers pin the redirect URI when a session is established,
// so the callback URL cannot change for existing sessions without
// breaking token refresh and forcing a re-auth.
// TODO(CODAGT-922): promote once existing sessions can be
// re-authenticated against a /api/v2 callback.
r.Get("/servers/{mcpServer}/oauth2/callback", api.mcpServerOAuth2Callback)
}
// Disconnect stays outside organization routes so former organization
// members can delete their stored token after losing config read access.
r.Delete("/servers/{mcpServer}/oauth2/disconnect", api.mcpServerOAuth2Disconnect)
}
func (api *API) registerUserAIProviderKeyRoutes(r chi.Router) {
r.Get("/", api.listUserAIProviderKeyConfigs)
r.Route("/{aiProvider}", func(r chi.Router) {
r.Put("/", api.upsertUserAIProviderKey)
r.Delete("/", api.deleteUserAIProviderKey)
})
}
// registerOrganizationChatRoutes mounts the organization-scoped chat and
// MCP server configuration routes; r must already extract the
// organization parameter.
func (api *API) registerOrganizationChatRoutes(r chi.Router) {
r.Route("/mcp-servers", func(r chi.Router) {
r.Get("/", api.listMCPServerConfigs)
r.Post("/", api.createMCPServerConfig)
r.Route("/{mcpserverconfig}", func(r chi.Router) {
r.With(httpmw.ExtractMCPServerConfigParam(api.Database, api.HTTPAuth.Authorize,
policy.ActionRead, policy.ActionUpdate, policy.ActionDelete)).Get("/", api.getMCPServerConfig)
r.With(httpmw.ExtractMCPServerConfigParam(api.Database, api.HTTPAuth.Authorize,
policy.ActionUpdate)).Patch("/", api.updateMCPServerConfig)
r.With(httpmw.ExtractMCPServerConfigParam(api.Database, api.HTTPAuth.Authorize,
policy.ActionDelete)).Delete("/", api.deleteMCPServerConfig)
r.With(httpmw.ExtractMCPServerConfigParam(api.Database, api.HTTPAuth.Authorize,
policy.ActionShare)).Get("/acl", api.mcpServerConfigACL)
r.With(httpmw.ExtractMCPServerConfigParam(api.Database, api.HTTPAuth.Authorize,
policy.ActionShare)).Patch("/acl", api.patchMCPServerConfigACL)
r.With(httpmw.ExtractMCPServerConfigParam(api.Database, api.HTTPAuth.Authorize,
policy.ActionRead)).Get("/oauth2/connect", api.mcpServerOAuth2Connect)
})
})
r.Route("/chats/model-overrides", func(r chi.Router) {
r.Get("/", api.getOrganizationChatModelOverrides)
r.Put("/{context}", api.putOrganizationChatModelOverride)
})
r.Route("/chats/models", func(r chi.Router) {
r.Get("/", api.listChatModelConfigsByOrganization)
r.Post("/", api.createChatModelConfig)
r.Route("/{model}", func(r chi.Router) {
r.Use(httpmw.ExtractChatModelConfigParam(api.Database))
r.Get("/", api.getChatModelConfig)
r.Patch("/", api.updateChatModelConfig)
r.Delete("/", api.deleteChatModelConfig)
r.Route("/acl", func(r chi.Router) {
r.Get("/", api.chatModelConfigACLHandler)
r.Patch("/", api.updateChatModelConfigACL)
})
})
})
}
func (api *API) registerOrganizationMemberChatRoutes(r chi.Router) {
r.Route("/chats/model-overrides", func(r chi.Router) {
r.Get("/", api.getUserChatPersonalModelOverrides)
r.Put("/{context}", api.putUserChatPersonalModelOverride)
})
}
+36
View File
@@ -0,0 +1,36 @@
package coderd
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
)
func TestChatFilesRateLimitMWCompatibilityAliases(t *testing.T) {
t.Parallel()
api := &API{Options: &Options{FilesRateLimit: 1}}
handler := api.chatFilesRateLimitMW()(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
for i, requestPath := range []string{
"/api/v2/chats/files/00000000-0000-0000-0000-000000000000",
"/api/experimental/chats/files/00000000-0000-0000-0000-000000000000",
} {
req := httptest.NewRequest(http.MethodGet, requestPath, nil)
req.RemoteAddr = "192.0.2.1:1234"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
resp := rec.Result()
_ = resp.Body.Close()
expectedStatus := http.StatusOK
if i > 0 {
expectedStatus = http.StatusTooManyRequests
}
require.Equal(t, expectedStatus, resp.StatusCode, requestPath)
}
}
+65
View File
@@ -0,0 +1,65 @@
package coderd_test
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/testutil"
)
func TestChatRoutesCompatibility(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, db := coderdtest.NewWithDatabase(t, nil)
firstUser := coderdtest.CreateFirstUser(t, client)
model := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
OrganizationID: firstUser.OrganizationID,
})
chat := dbgen.Chat(t, db, database.Chat{
OrganizationID: firstUser.OrganizationID,
OwnerID: firstUser.UserID,
LastModelConfigID: model.ID,
})
for _, route := range []string{
"/api/experimental/chats",
"/api/experimental/chats/config/system-prompt",
"/api/experimental/chats/models",
"/api/v2/chats",
"/api/v2/chats/config/system-prompt",
} {
res, err := client.Request(ctx, http.MethodGet, route, nil)
require.NoError(t, err)
_ = res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode, route)
}
for _, route := range []struct {
method string
path string
}{
{http.MethodGet, "/api/v2/chats/models"},
{http.MethodGet, "/api/v2/chats/model-configs"},
{http.MethodPost, "/api/v2/chats/model-configs"},
{http.MethodGet, "/api/v2/chats/providers"},
{http.MethodGet, "/api/v2/chats/user-provider-configs"},
{http.MethodGet, "/api/v2/chats/config/computer-use-provider"},
{http.MethodGet, "/api/v2/chats/config/advisor"},
{http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/debug/runs", chat.ID)},
{http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/stream/desktop", chat.ID)},
{http.MethodGet, "/api/v2/mcp/servers/not-a-uuid/oauth2/callback"},
{http.MethodPost, "/api/v2/mcp/http/server"},
} {
res, err := client.Request(ctx, route.method, route.path, nil)
require.NoError(t, err)
_ = res.Body.Close()
require.Equal(t, http.StatusNotFound, res.StatusCode, "%s %s", route.method, route.path)
}
}
+27 -199
View File
@@ -1369,220 +1369,33 @@ func New(options *Options) *API {
r.Delete("/", api.deleteUserSkill)
})
})
// Chat routes are promoted to /api/v2. CODAGT-921 decided a compatibility
// window, so these experimental duplicates must remain for one release.
// TODO(CODAGT-921): remove after the transition window (tracked in CODAGT-922).
r.Route("/users/{user}/ai-provider-keys", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractUserParam(options.Database),
)
r.Get("/", api.listUserAIProviderKeyConfigs)
r.Route("/{aiProvider}", func(r chi.Router) {
r.Put("/", api.upsertUserAIProviderKey)
r.Delete("/", api.deleteUserAIProviderKey)
})
})
r.Group(func(r chi.Router) {
r.Use(httpmw.RateLimit(options.FilesRateLimit, time.Minute))
r.Get("/chats/files/{file}/download", api.downloadChatFile)
api.registerUserAIProviderKeyRoutes(r)
})
r.Route("/organizations", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Route("/{organization}", func(r chi.Router) {
r.Use(httpmw.ExtractOrganizationParam(options.Database))
r.Route("/mcp-servers", func(r chi.Router) {
r.Get("/", api.listMCPServerConfigs)
r.Post("/", api.createMCPServerConfig)
r.Route("/{mcpserverconfig}", func(r chi.Router) {
r.With(httpmw.ExtractMCPServerConfigParam(options.Database, api.HTTPAuth.Authorize,
policy.ActionRead, policy.ActionUpdate, policy.ActionDelete)).Get("/", api.getMCPServerConfig)
r.With(httpmw.ExtractMCPServerConfigParam(options.Database, api.HTTPAuth.Authorize,
policy.ActionUpdate)).Patch("/", api.updateMCPServerConfig)
r.With(httpmw.ExtractMCPServerConfigParam(options.Database, api.HTTPAuth.Authorize,
policy.ActionDelete)).Delete("/", api.deleteMCPServerConfig)
r.With(httpmw.ExtractMCPServerConfigParam(options.Database, api.HTTPAuth.Authorize,
policy.ActionShare)).Get("/acl", api.mcpServerConfigACL)
r.With(httpmw.ExtractMCPServerConfigParam(options.Database, api.HTTPAuth.Authorize,
policy.ActionShare)).Patch("/acl", api.patchMCPServerConfigACL)
r.With(httpmw.ExtractMCPServerConfigParam(options.Database, api.HTTPAuth.Authorize,
policy.ActionRead)).Get("/oauth2/connect", api.mcpServerOAuth2Connect)
})
})
})
})
// Organization-scoped ChatModel management and runtime discovery.
// Keep the previous default-organization collection routes until the
// frontend uses the organization-scoped routes.
r.Route("/chats/model-configs", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
chi.RouteContext(req.Context()).URLParams.Add("organization", codersdk.DefaultOrganization)
next.ServeHTTP(rw, req)
})
},
httpmw.ExtractOrganizationParam(options.Database),
)
r.Get("/", api.listDefaultOrganizationChatModelConfigs)
r.Post("/", api.createChatModelConfig)
})
r.With(
apiKeyMiddleware,
func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
chi.RouteContext(req.Context()).URLParams.Add("organization", codersdk.DefaultOrganization)
next.ServeHTTP(rw, req)
})
},
httpmw.ExtractOrganizationParam(options.Database),
).Get("/chats/models", api.listChatModelConfigsByOrganization)
r.Route("/organizations/{organization}/chats/model-overrides", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractOrganizationParam(options.Database),
)
r.Get("/", api.getOrganizationChatModelOverrides)
r.Put("/{context}", api.putOrganizationChatModelOverride)
})
r.Route("/organizations/{organization}/members/{user}/chats/model-overrides", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractOrganizationParam(options.Database),
httpmw.ExtractOrganizationMemberParam(options.Database),
)
r.Get("/", api.getUserChatPersonalModelOverrides)
r.Put("/{context}", api.putUserChatPersonalModelOverride)
})
r.Route("/organizations/{organization}/chats/models", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.With(httpmw.ExtractOrganizationParam(options.Database)).Get("/", api.listChatModelConfigsByOrganization)
r.With(httpmw.ExtractOrganizationParam(options.Database)).Post("/", api.createChatModelConfig)
r.Route("/{model}", func(r chi.Router) {
r.Use(
httpmw.ExtractOrganizationParam(options.Database),
httpmw.ExtractChatModelConfigParam(options.Database),
)
r.Get("/", api.getChatModelConfig)
r.Patch("/", api.updateChatModelConfig)
r.Delete("/", api.deleteChatModelConfig)
r.Route("/acl", func(r chi.Router) {
r.Get("/", api.chatModelConfigACLHandler)
r.Patch("/", api.updateChatModelConfigACL)
})
})
})
r.Route("/chats", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
)
r.Get("/by-workspace", api.chatsByWorkspace)
r.Get("/", api.listChats)
r.Post("/", api.postChats)
r.Get("/watch", api.watchChats)
r.Route("/files", func(r chi.Router) {
r.Use(httpmw.RateLimit(options.FilesRateLimit, time.Minute))
r.Post("/", api.postChatFile)
r.Post("/{file}/download-url", api.postChatFileDownloadURL)
r.Get("/{file}", api.chatFileByID)
})
r.Route("/config", func(r chi.Router) {
r.Get("/system-prompt", api.getChatSystemPrompt)
r.Put("/system-prompt", api.putChatSystemPrompt)
r.Get("/plan-mode-instructions", api.getChatPlanModeInstructions)
r.Put("/plan-mode-instructions", api.putChatPlanModeInstructions)
r.Get("/personal-model-overrides", api.getChatPersonalModelOverridesAdminSettings)
r.Put("/personal-model-overrides", api.putChatPersonalModelOverridesAdminSettings)
r.Group(func(r chi.Router) {
r.Use(httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentChatVirtualDesktop))
r.Get("/computer-use-provider", api.getChatComputerUseProvider)
r.Put("/computer-use-provider", api.putChatComputerUseProvider)
})
r.Get("/debug-logging", api.getChatDebugLogging)
r.Put("/debug-logging", api.putChatDebugLogging)
r.Get("/user-debug-logging", api.getUserChatDebugLogging)
r.Put("/user-debug-logging", api.putUserChatDebugLogging)
r.Group(func(r chi.Router) {
r.Use(httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentChatAdvisor))
r.Get("/advisor", api.getChatAdvisorConfig)
r.Put("/advisor", api.putChatAdvisorConfig)
})
r.Get("/user-prompt", api.getUserChatCustomPrompt)
r.Put("/user-prompt", api.putUserChatCustomPrompt)
r.Get("/user-compaction-thresholds", api.getUserChatCompactionThresholds)
r.Put("/user-compaction-thresholds/{modelConfig}", api.putUserChatCompactionThreshold)
r.Delete("/user-compaction-thresholds/{modelConfig}", api.deleteUserChatCompactionThreshold)
r.Get("/workspace-ttl", api.getChatWorkspaceTTL)
r.Put("/workspace-ttl", api.putChatWorkspaceTTL)
r.Get("/retention-days", api.getChatRetentionDays)
r.Put("/retention-days", api.putChatRetentionDays)
r.Get("/debug-retention-days", api.getChatDebugRetentionDays)
r.Put("/debug-retention-days", api.putChatDebugRetentionDays)
r.Get("/auto-archive-days", api.getChatAutoArchiveDays)
r.Put("/auto-archive-days", api.putChatAutoArchiveDays)
})
// TODO(cian): place under /api/experimental/chats/config
r.Route("/providers", func(r chi.Router) {
r.Get("/", api.listChatProviders)
r.Post("/", api.createChatProvider)
r.Route("/{providerConfig}", func(r chi.Router) {
r.Patch("/", api.updateChatProvider)
r.Delete("/", api.deleteChatProvider)
})
})
r.Route("/user-provider-configs", func(r chi.Router) {
r.Get("/", api.listUserChatProviderConfigs)
r.Route("/{providerConfig}", func(r chi.Router) {
r.Put("/", api.upsertUserChatProviderKey)
r.Delete("/", api.deleteUserChatProviderKey)
})
})
r.Route("/{chat}", func(r chi.Router) {
r.Use(httpmw.ExtractChatParam(options.Database))
r.Route("/acl", func(r chi.Router) {
r.Get("/", api.getChatACL)
r.Patch("/", api.patchChatACL)
})
r.Get("/", api.getChat)
r.Patch("/", api.patchChat)
r.Get("/cost", api.getChatCost)
r.Get("/messages", api.getChatMessages)
r.Post("/messages", api.postChatMessages)
r.Patch("/messages/{message}", api.patchChatMessage)
r.Get("/prompts", api.getChatUserPrompts)
r.Route("/stream", func(r chi.Router) {
r.Get("/", api.streamChat)
r.Get("/parts", api.streamChatParts)
r.Get("/desktop", api.watchChatDesktop)
r.Get("/git", api.watchChatGit)
})
r.Post("/interrupt", api.interruptChat)
r.Post("/compact", api.compactChat)
r.Post("/reconcile-invalid", api.reconcileInvalidChatState)
r.Post("/tool-results", api.postChatToolResults)
r.Post("/title/propose", api.proposeChatTitle)
r.Get("/diff", api.getChatDiffContents)
r.Put("/context", api.refreshChatContext)
r.Route("/queue/{queuedMessage}", func(r chi.Router) {
r.Delete("/", api.deleteChatQueuedMessage)
r.Post("/promote", api.promoteChatQueuedMessage)
})
r.Route("/debug", func(r chi.Router) {
r.Get("/runs", api.getChatDebugRuns)
r.Get("/runs/{debugRun}", api.getChatDebugRun)
api.registerOrganizationChatRoutes(r)
r.Route("/members/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractOrganizationMemberParam(options.Database))
api.registerOrganizationMemberChatRoutes(r)
})
})
})
api.registerChatAPIRoutes(r, apiKeyMiddleware, chatAPIPrefixExperimental)
r.Route("/mcp", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
)
// This callback path is frozen because it is registered with OAuth2 providers.
r.Get("/servers/{mcpServer}/oauth2/callback", api.mcpServerOAuth2Callback)
// Disconnect stays outside organization routes so former organization
// members can delete their stored token after losing config read access.
r.Delete("/servers/{mcpServer}/oauth2/disconnect", api.mcpServerOAuth2Disconnect)
// MCP HTTP transport endpoint with mandatory authentication
r.Use(apiKeyMiddleware)
api.registerMCPServerOAuth2Routes(r, chatAPIPrefixExperimental)
// MCP HTTP transport endpoint with mandatory authentication.
r.Route("/http", func(r chi.Router) {
r.Use(httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentOAuth2, codersdk.ExperimentMCPServerHTTP))
r.Mount("/", api.mcpHTTPHandler())
@@ -1633,6 +1446,12 @@ func New(options *Options) *API {
r.Get("/available", handleExperimentsAvailable)
r.Get("/", api.handleExperimentsGet)
})
api.registerChatAPIRoutes(r, apiKeyMiddleware, chatAPIPrefixV2)
r.Route("/mcp", func(r chi.Router) {
r.Use(apiKeyMiddleware)
api.registerMCPServerOAuth2Routes(r, chatAPIPrefixV2)
})
r.Get("/updatecheck", api.updateCheck)
r.Route("/audit", func(r chi.Router) {
r.Use(
@@ -1695,6 +1514,7 @@ func New(options *Options) *API {
r.Use(
httpmw.ExtractOrganizationParam(options.Database),
)
api.registerOrganizationChatRoutes(r)
r.Get("/", api.organization)
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
r.Route("/templates", func(r chi.Router) {
@@ -1732,6 +1552,7 @@ func New(options *Options) *API {
r.Use(
httpmw.ExtractOrganizationMemberParam(options.Database),
)
api.registerOrganizationMemberChatRoutes(r)
r.Get("/", api.organizationMember)
r.Delete("/", api.deleteOrganizationMember)
r.Put("/roles", api.putMemberRoles)
@@ -1884,6 +1705,7 @@ func New(options *Options) *API {
r.Group(func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Route("/ai-provider-keys", api.registerUserAIProviderKeyRoutes)
r.Post("/convert-login", api.postConvertLoginType)
r.Delete("/", api.deleteUser)
r.Get("/", api.userByName)
@@ -2396,6 +2218,12 @@ type API struct {
ctx context.Context
cancel context.CancelFunc
// chatFilesRateLimit is shared by the /api/experimental and /api/v2
// chat file mounts so the compatibility window does not double the
// FilesRateLimit budget.
chatFilesRateLimitOnce sync.Once
chatFilesRateLimit func(http.Handler) http.Handler
// DeploymentID is loaded from the database on startup.
DeploymentID string
+32
View File
@@ -1,6 +1,7 @@
package coderdtest_test
import (
"encoding/json"
"go/ast"
"go/parser"
"go/token"
@@ -10,6 +11,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/apidoc"
"github.com/coder/coder/v2/coderd/coderdtest"
)
@@ -30,6 +32,36 @@ func TestEndpointsDocumented(t *testing.T) {
coderdtest.VerifySwaggerDefinitions(t, api.APIHandler, swaggerComments, coderdtest.WithSwaggerRoutePrefix("/api/v2"))
}
func TestChatModelPathParametersFormatted(t *testing.T) {
t.Parallel()
var swagger struct {
Paths map[string]map[string]struct {
Parameters []struct {
Name string `json:"name"`
In string `json:"in"`
Format string `json:"format"`
} `json:"parameters"`
} `json:"paths"`
}
require.NoError(t, json.Unmarshal([]byte(apidoc.SwaggerInfo.ReadDoc()), &swagger))
operations := swagger.Paths["/api/v2/organizations/{organization}/chats/models/{model}"]
for _, method := range []string{"get", "patch", "delete"} {
t.Run(method, func(t *testing.T) {
t.Parallel()
for _, parameter := range operations[method].Parameters {
if parameter.Name == "model" && parameter.In == "path" {
require.Equal(t, "uuid", parameter.Format)
return
}
}
require.Fail(t, "model path parameter not found")
})
}
}
func TestSDKFieldsFormatted(t *testing.T) {
t.Parallel()
+7 -4
View File
@@ -321,7 +321,7 @@ func assertGoCommentFirst(t *testing.T, comment SwaggerComment) {
text := strings.TrimSpace(line.Text)
if inSwaggerBlock {
if !strings.HasPrefix(text, "// @") && !strings.HasPrefix(text, "// nolint:") {
if text != "//" && !strings.HasPrefix(text, "// @") && !strings.HasPrefix(text, "// nolint:") && !strings.HasPrefix(text, "//nolint:") {
assert.Fail(t, "Go function comment must be placed before swagger comments")
return
}
@@ -369,7 +369,8 @@ func assertSecurityDefined(t *testing.T, comment SwaggerComment) {
comment.router == "/api/v2/users/login" ||
comment.router == "/api/v2/users/otp/request" ||
comment.router == "/api/v2/users/otp/change-password" ||
comment.router == "/api/v2/init-script/{os}/{arch}" {
comment.router == "/api/v2/init-script/{os}/{arch}" ||
comment.router == "/api/v2/chats/files/{file}/download" {
return // endpoints do not require authorization
}
if comment.router == "/api/v2/ai-gateway/serve" {
@@ -428,8 +429,10 @@ func assertProduce(t *testing.T, comment SwaggerComment) {
(comment.router == "/api/v2/workspaces/{workspace}/acl" && comment.method == "patch") ||
(comment.router == "/api/v2/init-script/{os}/{arch}" && comment.method == "get") ||
(comment.router == "/api/v2/organizations/{organization}/ai/spend/export" && comment.method == "get") ||
(comment.router == "/api/v2/templatebuilder/compose" && comment.method == "post") {
return // Exception: HTTP 200 is returned without response entity
(comment.router == "/api/v2/templatebuilder/compose" && comment.method == "post") ||
(comment.router == "/api/v2/chats/files/{file}" && comment.method == "get") ||
(comment.router == "/api/v2/chats/files/{file}/download" && comment.method == "get") {
return // Exception: HTTP 200 is returned without a response model
}
assert.Truef(t, comment.produce == "", "Response model is undefined, so we can't predict the content type: %v", comment)
+285 -153
View File
File diff suppressed because it is too large Load Diff
+2 -8
View File
@@ -26,8 +26,6 @@ import (
"github.com/coder/coder/v2/codersdk"
)
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
// @Summary Get chat ACLs
// @ID get-chat-acls
// @Security CoderSessionToken
@@ -35,9 +33,8 @@ import (
// @Produce json
// @Param chat path string true "Chat ID" format(uuid)
// @Success 200 {object} codersdk.ChatACL
// @Router /api/experimental/chats/{chat}/acl [get]
// @Router /api/v2/chats/{chat}/acl [get]
// @x-apidocgen {"skip": true}
// @Description Experimental: this endpoint is subject to change.
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getChatACL(rw http.ResponseWriter, r *http.Request) {
@@ -81,8 +78,6 @@ func (api *API) getChatACL(rw http.ResponseWriter, r *http.Request) {
})
}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
// @Summary Update chat ACL
// @ID update-chat-acl
// @Security CoderSessionToken
@@ -91,9 +86,8 @@ func (api *API) getChatACL(rw http.ResponseWriter, r *http.Request) {
// @Param chat path string true "Chat ID" format(uuid)
// @Param request body codersdk.UpdateChatACL true "Update chat ACL request"
// @Success 204
// @Router /api/experimental/chats/{chat}/acl [patch]
// @Router /api/v2/chats/{chat}/acl [patch]
// @x-apidocgen {"skip": true}
// @Description Experimental: this endpoint is subject to change.
func (api *API) patchChatACL(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
chat := httpmw.ChatParam(r)
+4 -8
View File
@@ -24,17 +24,15 @@ import (
"github.com/coder/coder/v2/codersdk"
)
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
// @Summary Get an AI model ACL
// @ID get-ai-model-acl
// @ID get-an-ai-model-acl
// @Security CoderSessionToken
// @Tags Chats
// @Produce json
// @Param organization path string true "Organization name or ID"
// @Param model path string true "Model ID" format(uuid)
// @Success 200 {object} codersdk.ChatModelACL
// @Router /api/experimental/organizations/{organization}/chats/models/{model}/acl [get]
// @Router /api/v2/organizations/{organization}/chats/models/{model}/acl [get]
// @x-apidocgen {"skip": true}
func (api *API) chatModelConfigACLHandler(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -55,10 +53,8 @@ func (*chatModelACLValidationError) Error() string {
return "invalid chat model ACL"
}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
// @Summary Update an AI model ACL
// @ID update-ai-model-acl
// @ID update-an-ai-model-acl
// @Security CoderSessionToken
// @Tags Chats
// @Accept json
@@ -66,7 +62,7 @@ func (*chatModelACLValidationError) Error() string {
// @Param model path string true "Model ID" format(uuid)
// @Param request body codersdk.UpdateChatModelACLRequest true "Sparse model ACL update"
// @Success 204
// @Router /api/experimental/organizations/{organization}/chats/models/{model}/acl [patch]
// @Router /api/v2/organizations/{organization}/chats/models/{model}/acl [patch]
// @x-apidocgen {"skip": true}
func (api *API) updateChatModelConfigACL(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
+1
View File
@@ -80,6 +80,7 @@ func Cors(allowAll bool, origins ...string) func(next http.Handler) http.Handler
// Use permissive CORS for OAuth2, MCP, and well-known endpoints
if strings.HasPrefix(r.URL.Path, "/oauth2/") ||
strings.HasPrefix(r.URL.Path, "/api/experimental/mcp/") ||
strings.HasPrefix(r.URL.Path, "/api/v2/mcp/") ||
strings.HasPrefix(r.URL.Path, "/.well-known/oauth-") {
permissiveCors(next).ServeHTTP(w, r)
return
+28 -1
View File
@@ -5,6 +5,7 @@ import (
"net/http"
"path"
"strconv"
"strings"
"sync/atomic"
"time"
@@ -22,6 +23,16 @@ import (
// RateLimit returns a handler that limits requests per-minute based
// on IP, endpoint, and user ID (if available).
func RateLimit(count int, window time.Duration) func(http.Handler) http.Handler {
return rateLimitWithEndpointKey(count, window, keyByNormalizedEndpoint)
}
// RateLimitByAPICompatibilityEndpoint shares rate-limit buckets for matching
// endpoints under the /api/v2 and /api/experimental compatibility prefixes.
func RateLimitByAPICompatibilityEndpoint(count int, window time.Duration) func(http.Handler) http.Handler {
return rateLimitWithEndpointKey(count, window, keyByAPICompatibilityEndpoint)
}
func rateLimitWithEndpointKey(count int, window time.Duration, endpointKey func(*http.Request) (string, error)) func(http.Handler) http.Handler {
// -1 is no rate limit
if count <= 0 {
return func(handler http.Handler) http.Handler {
@@ -86,7 +97,7 @@ func RateLimit(count int, window time.Duration) func(http.Handler) http.Handler
"%q provided but user is not %v",
codersdk.BypassRatelimitHeader, rbac.RoleOwner(),
)
}, keyByNormalizedEndpoint),
}, endpointKey),
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), w, http.StatusTooManyRequests, codersdk.Response{
Message: fmt.Sprintf("You've been rate limited for sending more than %v requests in %v.", count, window),
@@ -110,6 +121,22 @@ func keyByNormalizedEndpoint(r *http.Request) (string, error) {
return path.Clean(p), nil
}
func keyByAPICompatibilityEndpoint(r *http.Request) (string, error) {
p, err := keyByNormalizedEndpoint(r)
if err != nil {
return "", err
}
for _, prefix := range []string{"/api/v2", "/api/experimental"} {
if p == prefix {
return "/", nil
}
if strings.HasPrefix(p, prefix+"/") {
return strings.TrimPrefix(p, prefix), nil
}
}
return p, nil
}
// RateLimitByAuthToken returns a handler that limits requests based on the
// authentication token in the request.
//
+47
View File
@@ -79,6 +79,29 @@ func TestRateLimit(t *testing.T) {
}
})
t.Run("DifferentAPIPrefixes", func(t *testing.T) {
t.Parallel()
rtr := chi.NewRouter()
rtr.Use(httpmw.RateLimit(1, time.Second))
rtr.Get("/*", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
})
remoteAddr := randRemoteAddr()
for _, p := range []string{
"/api/v2/chats/providers",
"/api/experimental/chats/providers",
} {
req := httptest.NewRequest("GET", p, nil)
req.RemoteAddr = remoteAddr
rec := httptest.NewRecorder()
rtr.ServeHTTP(rec, req)
resp := rec.Result()
_ = resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode, p)
}
})
t.Run("RandomIPs", func(t *testing.T) {
t.Parallel()
rtr := chi.NewRouter()
@@ -178,6 +201,30 @@ func TestRateLimit(t *testing.T) {
})
}
func TestRateLimitByAPICompatibilityEndpoint(t *testing.T) {
t.Parallel()
rtr := chi.NewRouter()
rtr.Use(httpmw.RateLimitByAPICompatibilityEndpoint(1, time.Second))
rtr.Get("/*", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
})
remoteAddr := randRemoteAddr()
for i, p := range []string{
"/api/v2/chats/files/00000000-0000-0000-0000-000000000000",
"/api/experimental/chats/files/00000000-0000-0000-0000-000000000000",
} {
req := httptest.NewRequest("GET", p, nil)
req.RemoteAddr = remoteAddr
rec := httptest.NewRecorder()
rtr.ServeHTTP(rec, req)
resp := rec.Result()
_ = resp.Body.Close()
require.Equal(t, i != 0, resp.StatusCode == http.StatusTooManyRequests, p)
}
}
func TestRateLimitByAuthToken(t *testing.T) {
t.Parallel()
+23 -26
View File
@@ -145,11 +145,10 @@ func shouldRefreshOIDCToken(link database.UserLink) (bool, time.Time) {
// @Security CoderSessionToken
// @Tags MCP
// @Produce json
// @Param organization path string true "Organization ID" format(uuid)
// @Param organization path string true "Organization name or ID"
// @Success 200 {array} codersdk.MCPServerConfig
// @Router /api/experimental/organizations/{organization}/mcp-servers [get]
// @Router /api/v2/organizations/{organization}/mcp-servers [get]
// @x-apidocgen {"skip": true}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) listMCPServerConfigs(rw http.ResponseWriter, r *http.Request) {
@@ -286,12 +285,11 @@ func (api *API) mcpServerConfigReadInKeyScope(r *http.Request, organizationID uu
// @Tags MCP
// @Accept json
// @Produce json
// @Param organization path string true "Organization ID" format(uuid)
// @Param organization path string true "Organization name or ID"
// @Param request body codersdk.CreateMCPServerConfigRequest true "Create MCP server config request"
// @Success 201 {object} codersdk.MCPServerConfig
// @Router /api/experimental/organizations/{organization}/mcp-servers [post]
// @Router /api/v2/organizations/{organization}/mcp-servers [post]
// @x-apidocgen {"skip": true}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) createMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
@@ -514,12 +512,11 @@ func (api *API) createMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Tags MCP
// @Produce json
// @Param organization path string true "Organization ID" format(uuid)
// @Param organization path string true "Organization name or ID"
// @Param mcpserverconfig path string true "MCP server config ID" format(uuid)
// @Success 200 {object} codersdk.MCPServerConfig
// @Router /api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig} [get]
// @Router /api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig} [get]
// @x-apidocgen {"skip": true}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) getMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
@@ -602,13 +599,12 @@ func (api *API) getMCPServerConfigForMutation(rw http.ResponseWriter, r *http.Re
// @Tags MCP
// @Accept json
// @Produce json
// @Param organization path string true "Organization ID" format(uuid)
// @Param organization path string true "Organization name or ID"
// @Param mcpserverconfig path string true "MCP server config ID" format(uuid)
// @Param request body codersdk.UpdateMCPServerConfigRequest true "Update MCP server config request"
// @Success 200 {object} codersdk.MCPServerConfig
// @Router /api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig} [patch]
// @Router /api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig} [patch]
// @x-apidocgen {"skip": true}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) updateMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
@@ -968,12 +964,11 @@ func (api *API) updateMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
// @ID delete-mcp-server-config
// @Security CoderSessionToken
// @Tags MCP
// @Param organization path string true "Organization ID" format(uuid)
// @Param organization path string true "Organization name or ID"
// @Param mcpserverconfig path string true "MCP server config ID" format(uuid)
// @Success 204
// @Router /api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig} [delete]
// @Router /api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig} [delete]
// @x-apidocgen {"skip": true}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
func (api *API) deleteMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
auditor := api.Auditor.Load()
@@ -1022,17 +1017,17 @@ func (api *API) deleteMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNoContent)
}
// Redirects the user to the MCP server's OAuth2 authorization URL.
//
// @Summary Initiate MCP server OAuth2 connect
// @ID initiate-mcp-server-oauth2-connect
// @Security CoderSessionToken
// @Tags MCP
// @Param organization path string true "Organization ID" format(uuid)
// @Param organization path string true "Organization name or ID"
// @Param mcpserverconfig path string true "MCP server config ID" format(uuid)
// @Success 307
// @Router /api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/oauth2/connect [get]
// @Router /api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig}/oauth2/connect [get]
// @x-apidocgen {"skip": true}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
// Redirects the user to the MCP server's OAuth2 authorization URL.
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) mcpServerOAuth2Connect(rw http.ResponseWriter, r *http.Request) {
@@ -1111,21 +1106,21 @@ func (api *API) mcpServerOAuth2Connect(rw http.ResponseWriter, r *http.Request)
http.Redirect(rw, r, authURL, http.StatusTemporaryRedirect)
}
// Exchanges the authorization code for tokens and stores them.
//
// @Summary Handle MCP server OAuth2 callback
// @ID handle-mcp-server-oauth2-callback
// @Security CoderSessionToken
// @Tags MCP
// @Produce html
// @Param mcpServer path string true "MCP server config ID" format(uuid)
// @Param code query string false "Authorization code issued by the provider. Required together with state on success."
// @Param state query string false "Opaque state issued by the connect endpoint. Required together with code on success."
// @Param error query string false "Provider error code. Present instead of code when authorization fails."
// @Param error_description query string false "Provider error description accompanying error."
// @Produce text/html
// @Success 200
// @Router /api/experimental/mcp/servers/{mcpServer}/oauth2/callback [get]
// @x-apidocgen {"skip": true}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
// Exchanges the authorization code for tokens and stores them.
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) mcpServerOAuth2Callback(rw http.ResponseWriter, r *http.Request) {
@@ -1326,6 +1321,9 @@ func (api *API) mcpServerOAuth2Callback(rw http.ResponseWriter, r *http.Request)
</script></body></html>`))
}
// Removes the user's stored OAuth2 token for an MCP server.
// Provider revocation is best-effort and cannot block local deletion.
//
// @Summary Disconnect MCP server OAuth2 token
// @ID disconnect-mcp-server-oauth2-token
// @Security CoderSessionToken
@@ -1333,11 +1331,8 @@ func (api *API) mcpServerOAuth2Callback(rw http.ResponseWriter, r *http.Request)
// @Produce json
// @Param mcpServer path string true "MCP server config ID" format(uuid)
// @Success 200 {object} codersdk.MCPServerOAuth2DisconnectResponse
// @Router /api/experimental/mcp/servers/{mcpServer}/oauth2/disconnect [delete]
// @Router /api/v2/mcp/servers/{mcpServer}/oauth2/disconnect [delete]
// @x-apidocgen {"skip": true}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
// Removes the user's stored OAuth2 token for an MCP server.
// Provider revocation is best-effort and cannot block local deletion.
func (api *API) mcpServerOAuth2Disconnect(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -1569,6 +1564,8 @@ func (api *API) markMCPTokenRefreshFailure(
// external authorization servers, so it must not change when other MCP
// routes move. The route registration in coderd.go and the OAuth cookie
// Path values must stay aligned with it.
// TODO(CODAGT-922): define a migration story before moving registered
// redirect URIs to /api/v2.
func mcpServerOAuth2CallbackPath(configID uuid.UUID) string {
return fmt.Sprintf("/api/experimental/mcp/servers/%s/oauth2/callback", configID)
}
+4 -8
View File
@@ -21,17 +21,15 @@ import (
"github.com/coder/coder/v2/codersdk"
)
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
// @Summary Get MCP server config ACL
// @ID get-mcp-server-config-acl
// @Security CoderSessionToken
// @Tags MCP
// @Produce json
// @Param organization path string true "Organization ID" format(uuid)
// @Param organization path string true "Organization name or ID"
// @Param mcpserverconfig path string true "MCP server config ID" format(uuid)
// @Success 200 {object} codersdk.MCPServerConfigACL
// @Router /api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl [get]
// @Router /api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl [get]
// @x-apidocgen {"skip": true}
func (api *API) mcpServerConfigACL(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -58,18 +56,16 @@ func (api *API) mcpServerConfigACL(rw http.ResponseWriter, r *http.Request) {
})
}
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
// @Summary Update MCP server config ACL
// @ID update-mcp-server-config-acl
// @Security CoderSessionToken
// @Tags MCP
// @Accept json
// @Param organization path string true "Organization ID" format(uuid)
// @Param organization path string true "Organization name or ID"
// @Param mcpserverconfig path string true "MCP server config ID" format(uuid)
// @Param request body codersdk.UpdateMCPServerConfigACLRequest true "Update MCP server config ACL request"
// @Success 204
// @Router /api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl [patch]
// @Router /api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl [patch]
// @x-apidocgen {"skip": true}
func (api *API) patchMCPServerConfigACL(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
+4 -4
View File
@@ -1262,13 +1262,13 @@ WHERE id = ANY($1::uuid[]);
We make use of a relay mechanism when there are multiple coderd replicas. If a client connects to the stream endpoint on replica A, but the chat worker that owns the chat is on replica B, the endpoint will connect to replica B and relay streaming message parts.
There exists a `GET /api/experimental/chats/{chat}/stream/parts` endpoint that is responsible exclusively for streaming message parts. That endpoint talks to the chat worker on the same replica to obtain the message parts and relay them to the client.
There exists a `GET /api/v2/chats/{chat}/stream/parts` endpoint that is responsible exclusively for streaming message parts. That endpoint talks to the chat worker on the same replica to obtain the message parts and relay them to the client.
The flow is:
1. Client connects to the `GET /api/experimental/chats/{chat}/stream` endpoint.
1. Client connects to the `GET /api/v2/chats/{chat}/stream` endpoint.
2. The endpoint checks the database to see which replica owns the chat and resolves the replica's address.
3. The endpoint connects to the `GET /api/experimental/chats/{chat}/stream/parts` endpoint on that replica.
3. The endpoint connects to the `GET /api/v2/chats/{chat}/stream/parts` endpoint on that replica.
4. The stream endpoint relays both the full chat state and the streaming message parts to the client.
Some edge cases:
@@ -1298,7 +1298,7 @@ The parts endpoint is a WebSocket endpoint.
Connection setup:
- the URL identifies the chat ID, for example `GET /api/experimental/chats/{chat}/stream/parts`;
- the URL identifies the chat ID, for example `GET /api/v2/chats/{chat}/stream/parts`;
- the endpoint accepts the connection regardless of whether the local replica owns the chat;
- after connecting, the client sends control messages over the WebSocket to choose which episode it wants.
+4 -4
View File
@@ -1121,7 +1121,7 @@ type ChatDebugStep struct {
}
// DefaultChatWorkspaceTTL is the default TTL for chat workspaces.
// Zero means disabled — the template's own autostop setting applies.
// Zero means disabled; the template's own autostop setting applies.
const DefaultChatWorkspaceTTL = 0
// DefaultChatAutoArchiveDays is the default auto-archive window, in
@@ -1138,7 +1138,7 @@ const DefaultChatDebugRetentionDays int32 = 30
// workspace TTL setting.
type ChatWorkspaceTTLResponse struct {
// WorkspaceTTLMillis is the workspace TTL in milliseconds.
// Zero means disabled — the template's own autostop setting applies.
// Zero means disabled; the template's own autostop setting applies.
WorkspaceTTLMillis int64 `json:"workspace_ttl_ms"`
}
@@ -1146,7 +1146,7 @@ type ChatWorkspaceTTLResponse struct {
// workspace TTL setting.
type UpdateChatWorkspaceTTLRequest struct {
// WorkspaceTTLMillis is the workspace TTL in milliseconds.
// Zero means disabled — the template's own autostop setting applies.
// Zero means disabled; the template's own autostop setting applies.
WorkspaceTTLMillis int64 `json:"workspace_ttl_ms"`
}
@@ -1817,7 +1817,7 @@ type DynamicTool struct {
InputSchema json.RawMessage `json:"input_schema"`
// Handler executes the tool when the LLM invokes it.
// Not serialized — this only exists on the client side.
// Not serialized; this only exists on the client side.
Handler func(ctx context.Context, call DynamicToolCall) (DynamicToolResponse, error) `json:"-"`
}
+2 -2
View File
@@ -363,9 +363,9 @@ reject explicit model selection.
> [!NOTE]
> Both override layers may change between releases.
> Admin overrides are available through the API at
> `/api/experimental/organizations/{organization}/chats/model-overrides`
> `/api/v2/organizations/{organization}/chats/model-overrides`
> and personal overrides at
> `/api/experimental/organizations/{organization}/members/{user}/chats/model-overrides`.
> `/api/v2/organizations/{organization}/members/{user}/chats/model-overrides`.
## User API keys (BYOK)
@@ -199,7 +199,7 @@ Each server has a group and user ACL that controls which members can see and
use it. New servers grant read access to the organization's **Everyone** group,
so all members have access by default. Admins can remove the Everyone entry and
grant specific groups or users instead through the API
(`GET`/`PATCH /api/experimental/organizations/{organization}/mcp-servers/{id}/acl`); there is no ACL editor
(`GET`/`PATCH /api/v2/organizations/{organization}/mcp-servers/{id}/acl`); there is no ACL editor
in the settings page. ACL management is available in all editions and does not
require an enterprise entitlement. ACL changes are recorded in the audit log.
@@ -102,8 +102,8 @@ Refer to [Manage model permissions](../models.md#manage-model-permissions) for t
The **MCP servers** page has no access list editor.
Change an MCP server access list through the API instead:
- `GET /api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl`
- `PATCH /api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl`
- `GET /api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl`
- `PATCH /api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl`
## Related pages
@@ -47,21 +47,21 @@ Before mapping individual endpoints, understand the structural changes:
The table below maps each Tasks API endpoint to its Chats API equivalent.
| Operation | Tasks API | Chats API |
|-------------------|-------------------------------------------|-------------------------------------------------------------------|
| List | `GET /api/v2/tasks` | `GET /api/v2/chats` |
| Create | `POST /api/v2/tasks/{user}` | `POST /api/v2/chats` |
| Get by ID | `GET /api/v2/tasks/{user}/{task}` | `GET /api/v2/chats/{chat}` |
| Delete | `DELETE /api/v2/tasks/{user}/{task}` | `PATCH /api/v2/chats/{chat}` with `{"archived": true}` |
| Send follow-up | `POST /api/v2/tasks/{user}/{task}/send` | `POST /api/v2/chats/{chat}/messages` |
| Update input | `PATCH /api/v2/tasks/{user}/{task}/input` | `PATCH /api/v2/chats/{chat}/messages/{message}` |
| Get logs / stream | `GET /api/v2/tasks/{user}/{task}/logs` | `GET /api/v2/chats/{chat}/stream` (WebSocket) |
| Pause | `POST /api/v2/tasks/{user}/{task}/pause` | `POST /api/v2/chats/{chat}/interrupt` |
| Resume | `POST /api/v2/tasks/{user}/{task}/resume` | `POST /api/v2/chats/{chat}/messages` (send a new message) |
| Watch all | n/a | `GET /api/v2/chats/watch` (WebSocket) |
| Get messages | n/a | `GET /api/v2/chats/{chat}/messages` |
| List models | n/a | `GET /api/experimental/organizations/{organization}/chats/models` |
| Upload file | n/a | `POST /api/v2/chats/files` |
| Operation | Tasks API | Chats API |
|-------------------|-------------------------------------------|-----------------------------------------------------------|
| List | `GET /api/v2/tasks` | `GET /api/v2/chats` |
| Create | `POST /api/v2/tasks/{user}` | `POST /api/v2/chats` |
| Get by ID | `GET /api/v2/tasks/{user}/{task}` | `GET /api/v2/chats/{chat}` |
| Delete | `DELETE /api/v2/tasks/{user}/{task}` | `PATCH /api/v2/chats/{chat}` with `{"archived": true}` |
| Send follow-up | `POST /api/v2/tasks/{user}/{task}/send` | `POST /api/v2/chats/{chat}/messages` |
| Update input | `PATCH /api/v2/tasks/{user}/{task}/input` | `PATCH /api/v2/chats/{chat}/messages/{message}` |
| Get logs / stream | `GET /api/v2/tasks/{user}/{task}/logs` | `GET /api/v2/chats/{chat}/stream` (WebSocket) |
| Pause | `POST /api/v2/tasks/{user}/{task}/pause` | `POST /api/v2/chats/{chat}/interrupt` |
| Resume | `POST /api/v2/tasks/{user}/{task}/resume` | `POST /api/v2/chats/{chat}/messages` (send a new message) |
| Watch all | n/a | `GET /api/v2/chats/watch` (WebSocket) |
| Get messages | n/a | `GET /api/v2/chats/{chat}/messages` |
| List models | n/a | `GET /api/v2/organizations/{organization}/chats/models` |
| Upload file | n/a | `POST /api/v2/chats/files` |
## Migration steps
@@ -513,7 +513,7 @@ confirm the Chats API integration is working end-to-end.
List the available models in an organization to verify at least one provider is configured and reachable:
```sh
curl -s https://coder.example.com/api/experimental/organizations/$CODER_ORGANIZATION/chats/models \
curl -s https://coder.example.com/api/v2/organizations/$CODER_ORGANIZATION/chats/models \
-H "Coder-Session-Token: $CODER_SESSION_TOKEN" | jq '.models[].display_name'
```
+1313 -300
View File
File diff suppressed because it is too large Load Diff
+424
View File
@@ -227,6 +227,21 @@ title: Schemas
|--------------------|
| `prebuild_claimed` |
## coderd.chatsByWorkspaceResponse
```json
{
"property1": "string",
"property2": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|------------------|--------|----------|--------------|-------------|
| `[any property]` | string | false | | |
## coderd.cspViolation
```json
@@ -1336,6 +1351,32 @@ None
|------------|-----------------|----------|--------------|-------------|
| `warnings` | array of string | false | | |
## codersdk.AIProviderSummary
```json
{
"deleted": true,
"display_name": "string",
"enabled": true,
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
"type": "openai"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|----------------|----------------------------------------------------|----------|--------------|-------------|
| `deleted` | boolean | false | | |
| `display_name` | string | false | | |
| `enabled` | boolean | false | | |
| `icon` | string | false | | |
| `id` | string | false | | |
| `name` | string | false | | |
| `type` | [codersdk.AIProviderType](#codersdkaiprovidertype) | false | | |
## codersdk.AIProviderType
```json
@@ -2522,6 +2563,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `groups` | array of [codersdk.ChatGroup](#codersdkchatgroup) | false | | |
| `users` | array of [codersdk.ChatUser](#codersdkchatuser) | false | | |
## codersdk.ChatAutoArchiveDaysResponse
```json
{
"auto_archive_days": 0
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|---------------------|---------|----------|--------------|-------------|
| `auto_archive_days` | integer | false | | |
## codersdk.ChatBusyBehavior
```json
@@ -2721,6 +2776,36 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `total_cost_micros` | integer | false | | |
| `unpriced_request_count` | integer | false | | |
## codersdk.ChatDebugLoggingAdminSettings
```json
{
"allow_users": true,
"forced_by_deployment": true
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|------------------------|---------|----------|--------------|-------------|
| `allow_users` | boolean | false | | |
| `forced_by_deployment` | boolean | false | | |
## codersdk.ChatDebugRetentionDaysResponse
```json
{
"debug_retention_days": 0
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|------------------------|---------|----------|--------------|-------------|
| `debug_retention_days` | integer | false | | |
## codersdk.ChatDiffContents
```json
@@ -4538,6 +4623,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|-----------------------------------------------|
| `chat_default`, `deployment_default`, `model` |
## codersdk.ChatPersonalModelOverridesAdminSettings
```json
{
"allow_users": true
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|---------------|---------|----------|--------------|-------------|
| `allow_users` | boolean | false | | |
## codersdk.ChatPlanMode
```json
@@ -4552,6 +4651,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|----------|
| `plan` |
## codersdk.ChatPlanModeInstructionsResponse
```json
{
"plan_mode_instructions": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|--------------------------|--------|----------|--------------|-------------|
| `plan_mode_instructions` | string | false | | |
## codersdk.ChatPrompt
```json
@@ -5162,6 +5275,24 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `tool_call_id` | string | false | | |
| `tool_name` | string | false | | |
## codersdk.ChatSystemPromptResponse
```json
{
"default_system_prompt": "string",
"include_default_system_prompt": true,
"system_prompt": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|---------------------------------|---------|----------|--------------|-------------|
| `default_system_prompt` | string | false | | |
| `include_default_system_prompt` | boolean | false | | |
| `system_prompt` | string | false | | |
## codersdk.ChatUnsupportedProvider
```json
@@ -5345,6 +5476,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action_required`, `chat_summary_change`, `context_dirty`, `created`, `deleted`, `diff_status_change`, `status_change`, `summary_change`, `title_change` |
## codersdk.ChatWorkspaceTTLResponse
```json
{
"workspace_ttl_ms": 0
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|--------------------|---------|----------|--------------|--------------------------------------------------------------------------------------------------------------------------|
| `workspace_ttl_ms` | integer | false | | Workspace ttl ms is the workspace TTL in milliseconds. Zero means disabled; the template's own autostop setting applies. |
## codersdk.ClusterConfig
```json
@@ -6681,6 +6826,20 @@ This is required on creation to enable a user-flow of validating a template work
| `phone_number` | string | true | | |
| `source` | [codersdk.PremiumFunnelSource](#codersdkpremiumfunnelsource) | false | | Source is the premium paywall the request came from, for telemetry. It is not forwarded to the licensor. Omit it to report "direct". |
## codersdk.CreateUserAIProviderKeyRequest
```json
{
"api_key": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|-----------|--------|----------|--------------|-------------|
| `api_key` | string | false | | |
## codersdk.CreateUserRequestWithOrgs
```json
@@ -13492,6 +13651,28 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
|---------------|--------------------------------------------------------|----------|--------------|-------------|
| `usage_stats` | [codersdk.UsageStatsConfig](#codersdkusagestatsconfig) | false | | |
## codersdk.SubmitToolResultsRequest
```json
{
"results": [
{
"is_error": true,
"output": [
0
],
"tool_call_id": "string"
}
]
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|-----------|-----------------------------------------------------|----------|--------------|-------------|
| `results` | array of [codersdk.ToolResult](#codersdktoolresult) | false | | |
## codersdk.SupportConfig
```json
@@ -14999,6 +15180,26 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|----------------------|---------|----------|--------------|-------------|
| `max_token_lifetime` | integer | false | | |
## codersdk.ToolResult
```json
{
"is_error": true,
"output": [
0
],
"tool_call_id": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|----------------|------------------|----------|--------------|-------------|
| `is_error` | boolean | false | | |
| `output` | array of integer | false | | |
| `tool_call_id` | string | false | | |
## codersdk.TraceConfig
```json
@@ -15132,6 +15333,48 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
| `user_roles` | object | false | | |
| » `[any property]` | [codersdk.ChatRole](#codersdkchatrole) | false | | |
## codersdk.UpdateChatAutoArchiveDaysRequest
```json
{
"auto_archive_days": 0
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|---------------------|---------|----------|--------------|-------------|
| `auto_archive_days` | integer | false | | |
## codersdk.UpdateChatDebugLoggingAllowUsersRequest
```json
{
"allow_users": true
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|---------------|---------|----------|--------------|-------------|
| `allow_users` | boolean | false | | |
## codersdk.UpdateChatDebugRetentionDaysRequest
```json
{
"debug_retention_days": 0
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|------------------------|---------|----------|--------------|-------------|
| `debug_retention_days` | integer | false | | |
## codersdk.UpdateChatModelACLRequest
```json
@@ -15352,6 +15595,34 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
| `model` | string | false | | |
| `model_config` | [codersdk.ChatModelCallConfig](#codersdkchatmodelcallconfig) | false | | |
## codersdk.UpdateChatPersonalModelOverridesAdminSettingsRequest
```json
{
"allow_users": true
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|---------------|---------|----------|--------------|-------------|
| `allow_users` | boolean | false | | |
## codersdk.UpdateChatPlanModeInstructionsRequest
```json
{
"plan_mode_instructions": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|--------------------------|--------|----------|--------------|-------------|
| `plan_mode_instructions` | string | false | | |
## codersdk.UpdateChatRequest
```json
@@ -15394,6 +15665,36 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|------------------|---------|----------|--------------|-------------|
| `retention_days` | integer | false | | |
## codersdk.UpdateChatSystemPromptRequest
```json
{
"include_default_system_prompt": true,
"system_prompt": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|---------------------------------|---------|----------|--------------|-------------|
| `include_default_system_prompt` | boolean | false | | |
| `system_prompt` | string | false | | |
## codersdk.UpdateChatWorkspaceTTLRequest
```json
{
"workspace_ttl_ms": 0
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|--------------------|---------|----------|--------------|--------------------------------------------------------------------------------------------------------------------------|
| `workspace_ttl_ms` | integer | false | | Workspace ttl ms is the workspace TTL in milliseconds. Zero means disabled; the template's own autostop setting applies. |
## codersdk.UpdateCheckResponse
```json
@@ -15677,6 +15978,34 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
| `theme_light` | `dark`, `dark-protan-deuter`, `dark-tritan`, `light`, `light-protan-deuter`, `light-tritan` |
| `theme_mode` | `single`, `sync` |
## codersdk.UpdateUserChatCompactionThresholdRequest
```json
{
"threshold_percent": 100
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|---------------------|---------|----------|--------------|-------------|
| `threshold_percent` | integer | false | | |
## codersdk.UpdateUserChatDebugLoggingRequest
```json
{
"debug_logging_enabled": true
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|-------------------------|---------|----------|--------------|-------------|
| `debug_logging_enabled` | boolean | false | | |
## codersdk.UpdateUserChatPersonalModelOverrideRequest
```json
@@ -16185,6 +16514,34 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `updated_at` | string | false | | |
| `user_id` | string | false | | |
## codersdk.UserAIProviderKeyConfig
```json
{
"byok_enabled": true,
"has_provider_api_key": true,
"has_user_api_key": true,
"provider": {
"deleted": true,
"display_name": "string",
"enabled": true,
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
"type": "openai"
}
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|------------------------|----------------------------------------------------------|----------|--------------|-------------|
| `byok_enabled` | boolean | false | | |
| `has_provider_api_key` | boolean | false | | |
| `has_user_api_key` | boolean | false | | |
| `provider` | [codersdk.AIProviderSummary](#codersdkaiprovidersummary) | false | | |
## codersdk.UserAISpendStatus
```json
@@ -16321,6 +16678,73 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `theme_mode` | [codersdk.ThemeMode](#codersdkthememode) | false | | |
| `theme_preference` | string | false | | Theme preference is the legacy single-field appearance setting. In "single" mode it mirrors the active theme. In "sync" mode modern clients normally mirror the active OS slot, but older clients can update only this field, so it may diverge from ThemeLight or ThemeDark until a modern client saves the full appearance state again. |
## codersdk.UserChatCompactionThreshold
```json
{
"model_config_id": "f5fb4d91-62ca-4377-9ee6-5d43ba00d205",
"threshold_percent": 0
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|---------------------|---------|----------|--------------|-------------|
| `model_config_id` | string | false | | |
| `threshold_percent` | integer | false | | |
## codersdk.UserChatCompactionThresholds
```json
{
"thresholds": [
{
"model_config_id": "f5fb4d91-62ca-4377-9ee6-5d43ba00d205",
"threshold_percent": 0
}
]
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|--------------|---------------------------------------------------------------------------------------|----------|--------------|-------------|
| `thresholds` | array of [codersdk.UserChatCompactionThreshold](#codersdkuserchatcompactionthreshold) | false | | |
## codersdk.UserChatCustomPrompt
```json
{
"custom_prompt": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|-----------------|--------|----------|--------------|-------------|
| `custom_prompt` | string | false | | |
## codersdk.UserChatDebugLoggingSettings
```json
{
"debug_logging_enabled": true,
"forced_by_deployment": true,
"user_toggle_allowed": true
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|-------------------------|---------|----------|--------------|-------------|
| `debug_logging_enabled` | boolean | false | | |
| `forced_by_deployment` | boolean | false | | |
| `user_toggle_allowed` | boolean | false | | |
## codersdk.UserChatPersonalModelOverridesResponse
```json
@@ -1,8 +1,8 @@
# Example request using curl
{{ const rawBodyFile = data.operation['x-apidocgen'] && data.operation['x-apidocgen'].rawBodyFile; }}# Example request using curl
curl -X {{=data.methodUpper}} http://coder-server:8080{{=data.url}}{{=data.requiredQueryString}}{{?data.allHeaders.length}} \{{?}}
{{~data.allHeaders :p:index}}{{
if (p.name == "Content-Type" && p.exampleValues.object == "application/x-www-form-urlencoded") {
continue;
}
}} -H '{{=p.name}}: {{=p.exampleValues.object}}'{{?index < data.allHeaders.length-1}} \
{{?}}{{~}}
}} -H '{{=p.name}}: {{=p.exampleValues.object}}'{{?index < data.allHeaders.length-1 || rawBodyFile}} \
{{?}}{{~}}{{?rawBodyFile}} --data-binary '@{{=rawBodyFile}}'{{?}}
@@ -45,7 +45,7 @@
{{= renderDescription(data)}}
{{? data.operation.requestBody}}
{{? data.operation.requestBody && !(data.operation['x-apidocgen'] && data.operation['x-apidocgen'].rawBodyFile)}}
> Body parameter
{{? data.bodyParameter.exampleValues.description }}
+3 -3
View File
@@ -3657,7 +3657,7 @@ export const ChatWatchEventKinds: ChatWatchEventKind[] = [
export interface ChatWorkspaceTTLResponse {
/**
* WorkspaceTTLMillis is the workspace TTL in milliseconds.
* Zero means disabled — the template's own autostop setting applies.
* Zero means disabled; the template's own autostop setting applies.
*/
readonly workspace_ttl_ms: number;
}
@@ -4720,7 +4720,7 @@ export const DefaultChatDebugRetentionDays = 30;
// From codersdk/chats.go
/**
* DefaultChatWorkspaceTTL is the default TTL for chat workspaces.
* Zero means disabled — the template's own autostop setting applies.
* Zero means disabled; the template's own autostop setting applies.
*/
export const DefaultChatWorkspaceTTL = 0;
@@ -9925,7 +9925,7 @@ export interface UpdateChatSystemPromptRequest {
export interface UpdateChatWorkspaceTTLRequest {
/**
* WorkspaceTTLMillis is the workspace TTL in milliseconds.
* Zero means disabled — the template's own autostop setting applies.
* Zero means disabled; the template's own autostop setting applies.
*/
readonly workspace_ttl_ms: number;
}