From 6deb1038cd55c6fed5699813a6e214fd13e2af93 Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Mon, 24 Aug 2026 17:09:00 +0200
Subject: [PATCH 01/10] fix(coderd/x): surface Gemini malformed-function-call
stream deaths as retryable errors (#28470)
## Problem
When Gemini's OpenAI-compatible endpoint rejects a model-generated
function call server-side, it ends the SSE stream cleanly with the
nonstandard finish reason `function_call_filter:
MALFORMED_FUNCTION_CALL` after streaming only thought summaries; the
rejected call never reaches the wire. chatd treated this as a normal
completion: fantasy maps the unrecognized finish reason to `unknown`,
the reasoning block never closes (the only non-thought delta is the
`` marker, which the transport seam strips to an empty string,
and the openaicompat hook only ends reasoning on a non-empty content
delta), so the step accumulates no content and the generation loop
finishes the turn as complete. The user sees the model think for ~40
seconds and then nothing: no assistant message, no `last_error`, chat
status `waiting`. Observed twice in a row in production on
gemini-3.7-flash, with "Resume" reproducing it identically.
## Fix
Two independent layers:
- `coderd/x/googleopenai`: the stream rewrite now converts any chunk
whose `finish_reason` starts with `function_call_filter` into an
OpenAI-style SSE `{"error": ...}` event embedding the raw reason.
openai-go turns error-bearing events into stream errors, so the failure
rides the existing stream-error path instead of ending the stream
cleanly.
- `coderd/x/chatd/chaterror`: classifies that injected error as
retryable (kind `generic`, provider `google`) with a clear user-facing
message, so the existing generation retry machinery re-runs the step and
persists a `last_error` if retries exhaust.
- `coderd/x/chatd/chatloop`: provider-agnostic guard: a step that
produced no user-visible content and no tool calls under a finish reason
of `unknown`, `error`, `other`, or `tool-calls` (a tool-calls finish
that delivered zero calls) now returns a retryable error instead of
silently completing the turn. `stop` and `length` finishes keep their
existing semantics.
Tests cover the seam rewrite (live-capture SSE shape plus standard
finish reason passthrough), the new classification, and the chatloop
guard (error cases plus preserved stop, length, text, and tool-call
behavior). Each layer was red-green verified independently.
Remote dogfood UAT ran against this exact commit: normal reasoning and
tool-call chats on a real model complete cleanly with no spurious guard
errors and no retry loops. The Google-side failure itself is not
deterministically triggerable against live Gemini and is owned by the
unit tests.
> Xum acted on Mike's (@ibetitsmike) behalf.
(cherry picked from commit a48aedd924fe5aa824db40194cc893bf6b578aac)
---
coderd/x/chatd/chaterror/classify.go | 35 ++++
coderd/x/chatd/chaterror/classify_test.go | 27 +++
coderd/x/chatd/chatloop/chatloop.go | 45 ++++-
.../chatd/chatloop/nooutput_internal_test.go | 157 ++++++++++++++++++
coderd/x/googleopenai/thoughts.go | 51 +++++-
coderd/x/googleopenai/thoughts_test.go | 62 +++++++
6 files changed, 371 insertions(+), 6 deletions(-)
create mode 100644 coderd/x/chatd/chatloop/nooutput_internal_test.go
diff --git a/coderd/x/chatd/chaterror/classify.go b/coderd/x/chatd/chaterror/classify.go
index eb15093d036..b3a87269aaf 100644
--- a/coderd/x/chatd/chaterror/classify.go
+++ b/coderd/x/chatd/chaterror/classify.go
@@ -9,6 +9,7 @@ import (
"golang.org/x/net/http2"
"golang.org/x/xerrors"
+ "github.com/coder/coder/v2/coderd/x/googleopenai"
"github.com/coder/coder/v2/codersdk"
)
@@ -177,6 +178,15 @@ func Classify(err error) ClassifiedError {
return classified
}
+ if classified, ok := functionCallFilterClassification(
+ lower,
+ provider,
+ statusCode,
+ structured,
+ ); ok {
+ return classified
+ }
+
retryableHTTP2StreamReset, hasHTTP2StreamReset := classifyHTTP2StreamReset(err)
// combinedText merges the transport wrapper text with the structured
// provider response body so signal patterns in either are detected.
@@ -375,6 +385,31 @@ func streamIncompleteMessage(provider string) string {
return providerSubject(provider) + " stream closed unexpectedly before the response completed."
}
+// Match only the adapter's exact error prefix so unrelated provider errors
+// mentioning function_call_filter retain their normal classification.
+func functionCallFilterClassification(
+ lowerMessage string,
+ provider string,
+ statusCode int,
+ structured providerErrorDetails,
+) (ClassifiedError, bool) {
+ if !strings.Contains(lowerMessage, googleopenai.MalformedFunctionCallMessagePrefix) {
+ return ClassifiedError{}, false
+ }
+ if provider == "" {
+ provider = "google"
+ }
+ return normalizeClassification(ClassifiedError{
+ Message: "Gemini rejected the model's generated function call as malformed.",
+ Detail: structured.detail,
+ Kind: codersdk.ChatErrorKindGeneric,
+ Provider: provider,
+ Retryable: true,
+ StatusCode: statusCode,
+ RetryAfter: structured.retryAfter,
+ }), true
+}
+
func responsesAPIDiagnostic(lowerMessage, detail string) (string, bool) {
lowerDetail := strings.ToLower(detail)
for _, match := range responsesAPIDiagnosticMatches {
diff --git a/coderd/x/chatd/chaterror/classify_test.go b/coderd/x/chatd/chaterror/classify_test.go
index 42947a11914..f310f2d662f 100644
--- a/coderd/x/chatd/chaterror/classify_test.go
+++ b/coderd/x/chatd/chaterror/classify_test.go
@@ -79,6 +79,33 @@ func TestClassify(t *testing.T) {
StatusCode: 0,
},
},
+ {
+ name: "FunctionCallFilterMentionKeepsOwnClassification",
+ err: xerrors.New(`status 401: function_call_filter is not supported for this endpoint`),
+ want: chaterror.ClassifiedError{
+ Message: "Authentication with the AI provider failed. Check the API key and permissions.",
+ Kind: codersdk.ChatErrorKindAuth,
+ Provider: "",
+ Retryable: false,
+ StatusCode: 401,
+ },
+ },
+ {
+ name: "GeminiFunctionCallFilter",
+ err: xerrors.New(
+ `stream response: received error while streaming: ` +
+ `{"message":"gemini dropped the model's generated function call ` +
+ `(finish_reason \"function_call_filter: MALFORMED_FUNCTION_CALL\")",` +
+ `"type":"invalid_response_error","code":"malformed_function_call"}`,
+ ),
+ want: chaterror.ClassifiedError{
+ Message: "Gemini rejected the model's generated function call as malformed.",
+ Kind: codersdk.ChatErrorKindGeneric,
+ Provider: "google",
+ Retryable: true,
+ StatusCode: 0,
+ },
+ },
{
name: "AuthBeatsConfig",
err: xerrors.New("authentication failed: invalid model"),
diff --git a/coderd/x/chatd/chatloop/chatloop.go b/coderd/x/chatd/chatloop/chatloop.go
index 9a95a043c1a..f22cbc8d64b 100644
--- a/coderd/x/chatd/chatloop/chatloop.go
+++ b/coderd/x/chatd/chatloop/chatloop.go
@@ -51,6 +51,9 @@ var (
// classifiers blocked the response and the model produced no
// content, e.g. Anthropic's stop_reason "refusal".
ErrContentFiltered = xerrors.New("response blocked by provider content filter")
+ // ErrNoModelOutput is returned when a stream ends without visible content or
+ // tool calls and its finish reason indicates an incomplete response.
+ ErrNoModelOutput = xerrors.New("model stream finished without output")
errStreamSilenceTimeout = xerrors.New(
"chat stream was silent for longer than the configured timeout",
@@ -481,6 +484,12 @@ func GenerateAssistant(ctx context.Context, opts GenerateAssistantOptions) (Assi
if result.finishReason == fantasy.FinishReasonContentFilter && !hasUserVisibleContent(result.content) {
return AssistantOutcome{}, contentFilterError(errorProvider, result.providerMetadata)
}
+ // Treat discarded responses as retryable so an empty turn is not persisted.
+ if silentNoOutputFinish(result.finishReason) && !hasUserVisibleContent(result.content) && len(result.toolCalls) == 0 {
+ noOutputErr := noModelOutputError(errorProvider, result.finishReason)
+ opts.Metrics.RecordStreamRetry(provider, modelName, chaterror.Classify(noOutputErr))
+ return AssistantOutcome{}, noOutputErr
+ }
step := PersistedStep{
Content: result.content,
Usage: result.usage,
@@ -515,13 +524,20 @@ func wrapProviderStreamError(provider string, err error) error {
return xerrors.Errorf("stream response: %w", chaterror.WithClassification(err, classified))
}
-// hasUserVisibleContent reports whether any content part carries output the
-// user can see. Reasoning parts do not count: they stream transiently and are
-// not a substitute for a response.
+// hasUserVisibleContent ignores reasoning and blank text because neither can
+// complete a user-facing response.
func hasUserVisibleContent(content []fantasy.Content) bool {
for _, part := range content {
- switch part.(type) {
+ switch value := part.(type) {
case fantasy.ReasoningContent, *fantasy.ReasoningContent:
+ case fantasy.TextContent:
+ if strings.TrimSpace(value.Text) != "" {
+ return true
+ }
+ case *fantasy.TextContent:
+ if value != nil && strings.TrimSpace(value.Text) != "" {
+ return true
+ }
default:
return true
}
@@ -529,6 +545,27 @@ func hasUserVisibleContent(content []fantasy.Content) bool {
return false
}
+func silentNoOutputFinish(reason fantasy.FinishReason) bool {
+ switch reason {
+ case fantasy.FinishReasonUnknown, fantasy.FinishReasonError,
+ fantasy.FinishReasonOther, fantasy.FinishReasonToolCalls:
+ return true
+ default:
+ return false
+ }
+}
+
+func noModelOutputError(provider string, reason fantasy.FinishReason) error {
+ classified := chaterror.ClassifiedError{
+ Message: "The model ended its response without producing any output.",
+ Detail: "finish reason: " + string(reason),
+ Kind: codersdk.ChatErrorKindGeneric,
+ Provider: provider,
+ Retryable: true,
+ }
+ return chaterror.WithClassification(ErrNoModelOutput, classified)
+}
+
func contentFilterError(provider string, metadata fantasy.ProviderMetadata) error {
classified := chaterror.ClassifiedError{
Kind: codersdk.ChatErrorKindContentFilter,
diff --git a/coderd/x/chatd/chatloop/nooutput_internal_test.go b/coderd/x/chatd/chatloop/nooutput_internal_test.go
new file mode 100644
index 00000000000..d7f9622ca7b
--- /dev/null
+++ b/coderd/x/chatd/chatloop/nooutput_internal_test.go
@@ -0,0 +1,157 @@
+package chatloop
+
+import (
+ "context"
+ "testing"
+
+ "charm.land/fantasy"
+ "github.com/prometheus/client_golang/prometheus"
+ promtestutil "github.com/prometheus/client_golang/prometheus/testutil"
+ "github.com/stretchr/testify/require"
+
+ "github.com/coder/coder/v2/coderd/x/chatd/chaterror"
+ "github.com/coder/coder/v2/coderd/x/chatd/chattest"
+ "github.com/coder/coder/v2/codersdk"
+)
+
+func TestGenerateAssistant_SilentNoOutputFinish(t *testing.T) {
+ t.Parallel()
+
+ generate := func(t *testing.T, parts []fantasy.StreamPart) (AssistantOutcome, *Metrics, error) {
+ t.Helper()
+ model := &chattest.FakeModel{
+ ProviderName: "google",
+ ModelName: "test-model",
+ StreamFn: func(_ context.Context, _ fantasy.Call) (fantasy.StreamResponse, error) {
+ return streamFromParts(parts), nil
+ },
+ }
+ metrics := NewMetrics(prometheus.NewRegistry())
+ outcome, err := GenerateAssistant(context.Background(), GenerateAssistantOptions{
+ Model: model,
+ Metrics: metrics,
+ Messages: []fantasy.Message{
+ textMessage(fantasy.MessageRoleUser, "hello"),
+ },
+ })
+ return outcome, metrics, err
+ }
+
+ unterminatedReasoning := func(finish fantasy.FinishReason) []fantasy.StreamPart {
+ return []fantasy.StreamPart{
+ {Type: fantasy.StreamPartTypeReasoningStart, ID: "reasoning-1"},
+ {Type: fantasy.StreamPartTypeReasoningDelta, ID: "reasoning-1", Delta: "planning"},
+ {Type: fantasy.StreamPartTypeFinish, FinishReason: finish},
+ }
+ }
+
+ t.Run("UnknownFinishWithoutOutputErrors", func(t *testing.T) {
+ t.Parallel()
+
+ outcome, metrics, err := generate(t, unterminatedReasoning(fantasy.FinishReasonUnknown))
+ require.ErrorIs(t, err, ErrNoModelOutput)
+ require.Empty(t, outcome.Step.Content)
+
+ classified := chaterror.Classify(err)
+ require.True(t, classified.Retryable)
+ require.Equal(t, codersdk.ChatErrorKindGeneric, classified.Kind)
+ require.Equal(t, "google", classified.Provider)
+ require.Equal(t, "The model ended its response without producing any output.", classified.Message)
+ require.Equal(t, "finish reason: unknown", classified.Detail)
+
+ retries := promtestutil.ToFloat64(metrics.StreamRetriesTotal.WithLabelValues(
+ "google", "test-model", string(codersdk.ChatErrorKindGeneric),
+ ))
+ require.Equal(t, float64(1), retries)
+ })
+
+ t.Run("ReasoningOnlyContentWithUnknownFinishErrors", func(t *testing.T) {
+ t.Parallel()
+
+ _, _, err := generate(t, []fantasy.StreamPart{
+ {Type: fantasy.StreamPartTypeReasoningStart, ID: "reasoning-1"},
+ {Type: fantasy.StreamPartTypeReasoningDelta, ID: "reasoning-1", Delta: "planning"},
+ {Type: fantasy.StreamPartTypeReasoningEnd, ID: "reasoning-1"},
+ {Type: fantasy.StreamPartTypeFinish, FinishReason: fantasy.FinishReasonUnknown},
+ })
+ require.ErrorIs(t, err, ErrNoModelOutput)
+ })
+
+ t.Run("ToolCallsFinishWithoutToolCallsErrors", func(t *testing.T) {
+ t.Parallel()
+
+ _, _, err := generate(t, []fantasy.StreamPart{
+ {Type: fantasy.StreamPartTypeFinish, FinishReason: fantasy.FinishReasonToolCalls},
+ })
+ require.ErrorIs(t, err, ErrNoModelOutput)
+ require.Equal(t, "finish reason: tool-calls", chaterror.Classify(err).Detail)
+ })
+
+ t.Run("StopFinishWithoutOutputCompletes", func(t *testing.T) {
+ t.Parallel()
+
+ outcome, metrics, err := generate(t, unterminatedReasoning(fantasy.FinishReasonStop))
+ require.NoError(t, err)
+ require.Empty(t, outcome.Step.Content)
+ require.True(t, outcome.ModelStopped)
+
+ retries := promtestutil.ToFloat64(metrics.StreamRetriesTotal.WithLabelValues(
+ "google", "test-model", string(codersdk.ChatErrorKindGeneric),
+ ))
+ require.Equal(t, float64(0), retries)
+ })
+
+ t.Run("LengthFinishWithoutOutputCompletes", func(t *testing.T) {
+ t.Parallel()
+
+ _, _, err := generate(t, unterminatedReasoning(fantasy.FinishReasonLength))
+ require.NoError(t, err)
+ })
+
+ t.Run("EmptyTextWithUnknownFinishErrors", func(t *testing.T) {
+ t.Parallel()
+
+ _, _, err := generate(t, []fantasy.StreamPart{
+ {Type: fantasy.StreamPartTypeTextStart, ID: "text-1"},
+ {Type: fantasy.StreamPartTypeTextEnd, ID: "text-1"},
+ {Type: fantasy.StreamPartTypeFinish, FinishReason: fantasy.FinishReasonUnknown},
+ })
+ require.ErrorIs(t, err, ErrNoModelOutput)
+ })
+
+ t.Run("WhitespaceTextWithUnknownFinishErrors", func(t *testing.T) {
+ t.Parallel()
+
+ _, _, err := generate(t, []fantasy.StreamPart{
+ {Type: fantasy.StreamPartTypeTextStart, ID: "text-1"},
+ {Type: fantasy.StreamPartTypeTextDelta, ID: "text-1", Delta: " \n"},
+ {Type: fantasy.StreamPartTypeTextEnd, ID: "text-1"},
+ {Type: fantasy.StreamPartTypeFinish, FinishReason: fantasy.FinishReasonUnknown},
+ })
+ require.ErrorIs(t, err, ErrNoModelOutput)
+ })
+
+ t.Run("UnknownFinishWithTextCompletes", func(t *testing.T) {
+ t.Parallel()
+
+ outcome, _, err := generate(t, []fantasy.StreamPart{
+ {Type: fantasy.StreamPartTypeTextStart, ID: "text-1"},
+ {Type: fantasy.StreamPartTypeTextDelta, ID: "text-1", Delta: "answer"},
+ {Type: fantasy.StreamPartTypeTextEnd, ID: "text-1"},
+ {Type: fantasy.StreamPartTypeFinish, FinishReason: fantasy.FinishReasonUnknown},
+ })
+ require.NoError(t, err)
+ require.NotEmpty(t, outcome.Step.Content)
+ })
+
+ t.Run("ToolCallsFinishWithToolCallCompletes", func(t *testing.T) {
+ t.Parallel()
+
+ outcome, _, err := generate(t, []fantasy.StreamPart{
+ {Type: fantasy.StreamPartTypeToolCall, ID: "call-1", ToolCallName: "do_thing", ToolCallInput: "{}"},
+ {Type: fantasy.StreamPartTypeFinish, FinishReason: fantasy.FinishReasonToolCalls},
+ })
+ require.NoError(t, err)
+ require.Len(t, outcome.ToolCalls, 1)
+ })
+}
diff --git a/coderd/x/googleopenai/thoughts.go b/coderd/x/googleopenai/thoughts.go
index 613e71a785a..ea92be9d257 100644
--- a/coderd/x/googleopenai/thoughts.go
+++ b/coderd/x/googleopenai/thoughts.go
@@ -3,6 +3,7 @@ package googleopenai
import (
"bufio"
"bytes"
+ "encoding/json"
"errors"
"io"
"net/http"
@@ -145,6 +146,9 @@ func (b *thoughtStreamBody) rewriteLine(line []byte) []byte {
if !choices.IsArray() {
return line
}
+ if errPayload := functionCallFilterErrorPayload(choices); errPayload != nil {
+ return assembleDataLine(errPayload, suffix)
+ }
out := payload
for index, choice := range choices.Array() {
delta := choice.Get("delta")
@@ -186,9 +190,52 @@ func (b *thoughtStreamBody) rewriteLine(line []byte) []byte {
b.inThought[index] = false
}
}
- rewritten := make([]byte, 0, len(streamDataPrefix)+len(out)+len(suffix))
+ return assembleDataLine(out, suffix)
+}
+
+func assembleDataLine(payload, suffix []byte) []byte {
+ rewritten := make([]byte, 0, len(streamDataPrefix)+len(payload)+len(suffix))
rewritten = append(rewritten, streamDataPrefix...)
- rewritten = append(rewritten, out...)
+ rewritten = append(rewritten, payload...)
rewritten = append(rewritten, suffix...)
return rewritten
}
+
+// Gemini uses this nonstandard finish-reason prefix when it discards a
+// malformed generated function call.
+const functionCallFilterFinishReasonPrefix = "function_call_filter"
+
+// MalformedFunctionCallMessagePrefix distinguishes adapter-generated errors
+// from unrelated provider errors.
+const MalformedFunctionCallMessagePrefix = "gemini dropped the model's generated function call"
+
+type streamErrorEvent struct {
+ Error streamErrorDetail `json:"error"`
+}
+
+type streamErrorDetail struct {
+ Message string `json:"message"`
+ Type string `json:"type"`
+ Code string `json:"code"`
+}
+
+// functionCallFilterErrorPayload turns the nonstandard finish reason into an
+// SSE error so consumers do not accept the empty stream as successful.
+func functionCallFilterErrorPayload(choices gjson.Result) []byte {
+ for _, choice := range choices.Array() {
+ reason := choice.Get("finish_reason")
+ if reason.Type != gjson.String || !strings.HasPrefix(reason.Str, functionCallFilterFinishReasonPrefix) {
+ continue
+ }
+ payload, err := json.Marshal(streamErrorEvent{Error: streamErrorDetail{
+ Message: MalformedFunctionCallMessagePrefix + " (finish_reason " + strconv.Quote(reason.Str) + ")",
+ Type: "invalid_response_error",
+ Code: "malformed_function_call",
+ }})
+ if err != nil {
+ return nil
+ }
+ return payload
+ }
+ return nil
+}
diff --git a/coderd/x/googleopenai/thoughts_test.go b/coderd/x/googleopenai/thoughts_test.go
index 62e4e7b8957..938ccb7ebd1 100644
--- a/coderd/x/googleopenai/thoughts_test.go
+++ b/coderd/x/googleopenai/thoughts_test.go
@@ -81,6 +81,68 @@ func TestRewriteThoughtResponse_StreamWithoutThoughts(t *testing.T) {
require.Equal(t, body, string(rewritten))
}
+func TestRewriteThoughtResponse_StreamFunctionCallFilter(t *testing.T) {
+ t.Parallel()
+
+ lines := []string{
+ `data: {"choices":[{"delta":{"role":"assistant","content":"**Planning**\n\nStep one.","extra_content":{"google":{"thought":true}}},"index":0}],"object":"chat.completion.chunk"}`,
+ ``,
+ `data: {"choices":[{"delta":{"content":"","extra_content":null},"index":0}],"object":"chat.completion.chunk"}`,
+ ``,
+ `data: {"choices":[{"delta":{"content":null},"finish_reason":"function_call_filter: MALFORMED_FUNCTION_CALL","index":0}],"usage":{"completion_tokens":3392,"prompt_tokens":100,"total_tokens":3492}}`,
+ ``,
+ `data: [DONE]`,
+ ``,
+ }
+ resp := &http.Response{
+ StatusCode: http.StatusOK,
+ Header: http.Header{"Content-Type": []string{"text/event-stream"}},
+ Body: io.NopCloser(strings.NewReader(strings.Join(lines, "\n"))),
+ }
+
+ googleopenai.RewriteThoughtResponse(resp)
+ rewritten, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.NoError(t, resp.Body.Close())
+ out := strings.Split(string(rewritten), "\n")
+ require.Len(t, out, len(lines))
+
+ first := gjson.Get(strings.TrimPrefix(out[0], "data: "), "choices.0.delta")
+ require.Equal(t, "**Planning**\n\nStep one.", first.Get("reasoning_content").String())
+
+ require.True(t, strings.HasPrefix(out[4], "data: "))
+ errPayload := gjson.Get(strings.TrimPrefix(out[4], "data: "), "error")
+ require.True(t, errPayload.Exists())
+ require.Contains(t, errPayload.Get("message").String(), `"function_call_filter: MALFORMED_FUNCTION_CALL"`)
+ require.Equal(t, "invalid_response_error", errPayload.Get("type").String())
+ require.Equal(t, "malformed_function_call", errPayload.Get("code").String())
+
+ require.Equal(t, `data: [DONE]`, out[6])
+}
+
+func TestRewriteThoughtResponse_StreamStandardFinishReasonsPassThrough(t *testing.T) {
+ t.Parallel()
+
+ for _, reason := range []string{"stop", "tool_calls", "length", "content_filter"} {
+ t.Run(reason, func(t *testing.T) {
+ t.Parallel()
+
+ line := `data: {"choices":[{"delta":{"content":null},"finish_reason":"` + reason + `","index":0}]}` + "\n"
+ resp := &http.Response{
+ StatusCode: http.StatusOK,
+ Header: http.Header{"Content-Type": []string{"text/event-stream"}},
+ Body: io.NopCloser(strings.NewReader(line)),
+ }
+
+ googleopenai.RewriteThoughtResponse(resp)
+ rewritten, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.NoError(t, resp.Body.Close())
+ require.Equal(t, line, string(rewritten))
+ })
+ }
+}
+
func TestRewriteThoughtResponse_StreamToolCallsEndThought(t *testing.T) {
t.Parallel()
From ccabdf14f0cd01366a322ba012893db5a43c145e Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Mon, 24 Aug 2026 17:39:52 +0200
Subject: [PATCH 02/10] fix: apply MCP server selection when editing a chat
message (#28471)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Problem
Editing a user message in an Agent chat silently dropped the MCP server
selection. The composer renders the MCP picker in edit mode and toggles
update local client state, so the picker displayed the new selection,
but the edit request omitted `mcp_server_ids` at every layer (frontend
request builder, `codersdk.EditChatMessageRequest`, the `PATCH
/chats/{chat}/messages/{message}` handler, and `chatd.EditMessage`). The
chat's persisted selection never changed and the regenerated turn ran
without the newly enabled MCP tools. Two dogfood users hit this within
hours; there is no error anywhere and the UI shows the opposite of the
server state.
## Changes
- `codersdk`: add `MCPServerIDs *[]uuid.UUID` to
`EditChatMessageRequest`, mirroring `CreateChatMessageRequest` (nil
preserves the current selection).
- `chatd`: extract the send path's MCP update block into one shared
`applyRequestedMCPServerIDs` helper (explore-subagent snapshot
immutability guard plus Force On enforcement, Cure53 CDM-02-010) and
call it from both `SendMessage` and `EditMessage`, so enforcement cannot
drift between the two paths.
- `coderd`: extract the send handler's request validation (dedupe,
enabled-in-organization check, persisted-ID exemption) into
`normalizeRequestedChatMCPServerIDs` and wire it into the edit handler,
which now threads the selection into `EditMessageOptions`.
- Frontend: the edit request now includes `mcp_server_ids:
[...effectiveMCPServerIds]`, exactly like the send path, making the
picker's displayed state real.
- `make gen` artifacts (swagger, API docs, `typesGenerated.ts`).
## Tests
Each layer is covered and was proven with independent red toggles
(removing one layer's wiring fails only that layer's tests):
- chatd (`TestEditMessage_MCPServerIDs`): edit applies a provided
selection, nil preserves it, an emptied list cannot remove a `force_on`
server, and explore subagent chats keep the spawn-time snapshot.
- API (`TestPatchChatMessage/MCPServerIDsApplied`,
`MCPServerIDsInvalidRejected`): persistence via the endpoint, omission
preserves, unknown IDs get the same 400 as the send path.
- Storybook (`EditAppliesMCPServerSelection`): toggling a server on
during an edit puts it in the edit request payload.
Note: the `AgentChatPage.stories.tsx` story "Queued For Capacity After
Polling" fails locally on current main as well (verified against the
main baseline with this branch's changes reverted); it is unrelated to
this diff.
Remote dogfood UAT ran on the exact head and passed, including proof
that the regenerated turn actually gains the newly enabled MCP server's
tools.
> 🤖 Xum acted on Mike's (@ibetitsmike) behalf. • Model:
`anthropic:claude-fable-5`
(cherry picked from commit b6d76532311f8010778721fc9fe2448e2fb3e1b3)
---
coderd/apidoc/docs.go | 8 +
coderd/apidoc/swagger.json | 8 +
coderd/exp_chats.go | 77 ++++++----
coderd/exp_chats_test.go | 92 ++++++++++++
coderd/x/chatd/chatd.go | 66 +++++---
coderd/x/chatd/chatd_test.go | 141 ++++++++++++++++++
codersdk/chats.go | 4 +
docs/reference/api/chats.md | 3 +
docs/reference/api/schemas.md | 4 +
site/src/api/typesGenerated.ts | 6 +
.../AgentsPage/AgentChatPage.stories.tsx | 73 +++++++++
site/src/pages/AgentsPage/AgentChatPage.tsx | 1 +
12 files changed, 430 insertions(+), 53 deletions(-)
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 56239a19d5f..18247541e41 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -22005,6 +22005,14 @@ const docTemplate = `{
"$ref": "#/definitions/codersdk.ChatInputPart"
}
},
+ "mcp_server_ids": {
+ "description": "MCPServerIDs, when set, replaces the chat's MCP server selection\nbefore the replacement turn runs. When nil the current selection\nis preserved.",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
"model_config_id": {
"description": "ModelConfigID, when set, overrides the model used for the\nreplacement user message and the assistant turn that follows.\nWhen nil the original message's model is preserved.",
"type": "string",
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 6ab6ba14775..cd2a051eeb9 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -20041,6 +20041,14 @@
"$ref": "#/definitions/codersdk.ChatInputPart"
}
},
+ "mcp_server_ids": {
+ "description": "MCPServerIDs, when set, replaces the chat's MCP server selection\nbefore the replacement turn runs. When nil the current selection\nis preserved.",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
"model_config_id": {
"description": "ModelConfigID, when set, overrides the model used for the\nreplacement user message and the assistant turn that follows.\nWhen nil the original message's model is preserved.",
"type": "string",
diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go
index 9b6d5015861..a7a093e3987 100644
--- a/coderd/exp_chats.go
+++ b/coderd/exp_chats.go
@@ -1139,6 +1139,42 @@ func validateChatMCPServerIDs(
return unique, invalid, nil
}
+// normalizeRequestedChatMCPServerIDs validates a request's MCP server
+// selection for an existing chat. When requested is nil there is no
+// change to make. IDs already persisted on the chat are exempt from the
+// enabled-in-organization check: a server that is disabled or revoked
+// after selection must not block sends. The generation path skips
+// servers the chat can no longer use, and keeping the ID preserves the
+// selection if the server is re-enabled. A non-nil response indicates
+// the caller must write it with the returned status and stop.
+func (api *API) normalizeRequestedChatMCPServerIDs(ctx context.Context, chat database.Chat, requested *[]uuid.UUID) (*[]uuid.UUID, int, *codersdk.Response) {
+ if requested == nil {
+ return nil, 0, nil
+ }
+ normalized, invalid, err := validateChatMCPServerIDs(ctx, api.Database, chat.OrganizationID, *requested)
+ if err != nil {
+ return nil, http.StatusInternalServerError, &codersdk.Response{
+ Message: "Failed to validate MCP server IDs.",
+ Detail: err.Error(),
+ }
+ }
+ persisted := make(map[uuid.UUID]struct{}, len(chat.MCPServerIDs))
+ for _, id := range chat.MCPServerIDs {
+ persisted[id] = struct{}{}
+ }
+ newlyInvalid := make([]uuid.UUID, 0, len(invalid))
+ for _, id := range invalid {
+ if _, ok := persisted[id]; !ok {
+ newlyInvalid = append(newlyInvalid, id)
+ }
+ }
+ if len(newlyInvalid) > 0 {
+ resp := invalidChatMCPServerIDsResponse(newlyInvalid)
+ return nil, http.StatusBadRequest, &resp
+ }
+ return &normalized, 0, nil
+}
+
func invalidChatMCPServerIDsResponse(ids []uuid.UUID) codersdk.Response {
invalid := make([]string, 0, len(ids))
for _, id := range ids {
@@ -2592,36 +2628,12 @@ func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) {
return
}
- if req.MCPServerIDs != nil {
- normalizedMCPServerIDs, invalidMCPServerIDs, err := validateChatMCPServerIDs(ctx, api.Database, chat.OrganizationID, *req.MCPServerIDs)
- if err != nil {
- httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
- Message: "Failed to validate MCP server IDs.",
- Detail: err.Error(),
- })
- return
- }
- req.MCPServerIDs = &normalizedMCPServerIDs
- // IDs already persisted on the chat are exempt: a server that
- // is disabled or revoked after selection must not block sends.
- // The generation path skips servers the chat can no longer use,
- // and keeping the ID preserves the selection if the server is
- // re-enabled.
- persisted := make(map[uuid.UUID]struct{}, len(chat.MCPServerIDs))
- for _, id := range chat.MCPServerIDs {
- persisted[id] = struct{}{}
- }
- newlyInvalid := make([]uuid.UUID, 0, len(invalidMCPServerIDs))
- for _, id := range invalidMCPServerIDs {
- if _, ok := persisted[id]; !ok {
- newlyInvalid = append(newlyInvalid, id)
- }
- }
- if len(newlyInvalid) > 0 {
- httpapi.Write(ctx, rw, http.StatusBadRequest, invalidChatMCPServerIDsResponse(newlyInvalid))
- return
- }
+ normalizedMCPServerIDs, status, mcpResp := api.normalizeRequestedChatMCPServerIDs(ctx, chat, req.MCPServerIDs)
+ if mcpResp != nil {
+ httpapi.Write(ctx, rw, status, *mcpResp)
+ return
}
+ req.MCPServerIDs = normalizedMCPServerIDs
if req.PlanMode != nil {
if !validateChatPlanMode(*req.PlanMode) {
@@ -2840,6 +2852,12 @@ func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) {
return
}
+ editMCPServerIDs, status, mcpResp := api.normalizeRequestedChatMCPServerIDs(ctx, chat, req.MCPServerIDs)
+ if mcpResp != nil {
+ httpapi.Write(ctx, rw, status, *mcpResp)
+ return
+ }
+
editResult, editErr := api.chatDaemon.EditMessage(ctx, chatd.EditMessageOptions{
ChatID: chat.ID,
CreatedBy: apiKey.UserID,
@@ -2847,6 +2865,7 @@ func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) {
Content: contentBlocks,
ModelConfigID: editModelConfigID,
ReasoningEffort: editReasoningEffort,
+ MCPServerIDs: editMCPServerIDs,
})
if editErr != nil {
if writeChatHookErr(ctx, rw, editErr, "Chat message denied by lifecycle hook.") {
diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go
index ffe40485d92..e1788aa8ab6 100644
--- a/coderd/exp_chats_test.go
+++ b/coderd/exp_chats_test.go
@@ -10101,6 +10101,98 @@ func TestPatchChatMessage(t *testing.T) {
require.False(t, foundOriginalInChat)
})
+ t.Run("MCPServerIDsApplied", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ client, db := newChatClientWithDatabase(t)
+ firstUser := coderdtest.CreateFirstUser(t, client.Client)
+ _ = createChatModel(t, client)
+
+ orgConfig := dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
+ OrganizationID: firstUser.OrganizationID,
+ Enabled: true,
+ })
+
+ chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
+ OrganizationID: firstUser.OrganizationID,
+ Content: []codersdk.ChatInputPart{{
+ Type: codersdk.ChatInputPartTypeText,
+ Text: "before mcp edit",
+ }},
+ })
+ require.NoError(t, err)
+ require.Empty(t, chat.MCPServerIDs)
+
+ messagesResult, err := client.GetChatMessages(ctx, chat.ID, nil)
+ require.NoError(t, err)
+ userMessageID := messagesResult.Messages[0].ID
+
+ edited, err := client.EditChatMessage(ctx, chat.ID, userMessageID, codersdk.EditChatMessageRequest{
+ Content: []codersdk.ChatInputPart{{
+ Type: codersdk.ChatInputPartTypeText,
+ Text: "edit enabling the mcp server",
+ }},
+ MCPServerIDs: &[]uuid.UUID{orgConfig.ID},
+ })
+ require.NoError(t, err)
+
+ storedChat, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID)
+ require.NoError(t, err)
+ require.Equal(t, []uuid.UUID{orgConfig.ID}, storedChat.MCPServerIDs)
+
+ // Omitting the field preserves the persisted selection.
+ _, err = client.EditChatMessage(ctx, chat.ID, edited.Message.ID, codersdk.EditChatMessageRequest{
+ Content: []codersdk.ChatInputPart{{
+ Type: codersdk.ChatInputPartTypeText,
+ Text: "edit without an mcp selection",
+ }},
+ })
+ require.NoError(t, err)
+
+ storedChat, err = db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID)
+ require.NoError(t, err)
+ require.Equal(t, []uuid.UUID{orgConfig.ID}, storedChat.MCPServerIDs)
+ })
+
+ t.Run("MCPServerIDsInvalidRejected", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ client, db := newChatClientWithDatabase(t)
+ firstUser := coderdtest.CreateFirstUser(t, client.Client)
+ _ = createChatModel(t, client)
+
+ chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
+ OrganizationID: firstUser.OrganizationID,
+ Content: []codersdk.ChatInputPart{{
+ Type: codersdk.ChatInputPartTypeText,
+ Text: "before invalid mcp edit",
+ }},
+ })
+ require.NoError(t, err)
+
+ messagesResult, err := client.GetChatMessages(ctx, chat.ID, nil)
+ require.NoError(t, err)
+ userMessageID := messagesResult.Messages[0].ID
+
+ unknownID := uuid.New()
+ _, err = client.EditChatMessage(ctx, chat.ID, userMessageID, codersdk.EditChatMessageRequest{
+ Content: []codersdk.ChatInputPart{{
+ Type: codersdk.ChatInputPartTypeText,
+ Text: "edit selecting an unknown mcp server",
+ }},
+ MCPServerIDs: &[]uuid.UUID{unknownID},
+ })
+ sdkErr := requireSDKError(t, err, http.StatusBadRequest)
+ require.Equal(t, "One or more MCP server IDs are invalid or disabled.", sdkErr.Message)
+ require.Equal(t, "Invalid IDs: "+unknownID.String(), sdkErr.Detail)
+
+ storedChat, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID)
+ require.NoError(t, err)
+ require.Empty(t, storedChat.MCPServerIDs)
+ })
+
t.Run("CrossOrgModelConfigRejected", func(t *testing.T) {
t.Parallel()
diff --git a/coderd/x/chatd/chatd.go b/coderd/x/chatd/chatd.go
index 6034355c5a1..6e11785de09 100644
--- a/coderd/x/chatd/chatd.go
+++ b/coderd/x/chatd/chatd.go
@@ -1172,6 +1172,10 @@ type EditMessageOptions struct {
// original message's model is preserved.
ModelConfigID uuid.UUID
ReasoningEffort *string
+ // MCPServerIDs, when non-nil, replaces the chat's MCP server
+ // selection before the replacement turn runs. When nil the
+ // current selection is preserved.
+ MCPServerIDs *[]uuid.UUID
}
// EditMessageResult contains the replacement user message and chat status.
@@ -1242,6 +1246,36 @@ func enforceForcedMCPServerIDs(ctx context.Context, store database.Store, organi
return merged, nil
}
+// applyRequestedMCPServerIDs replaces the chat's MCP server selection
+// inside the state-machine transaction when a request provides one.
+// Explore child chats keep the spawn-time snapshot immutable. Force On
+// MCP servers are enforced server-side so a caller cannot remove them
+// by tampering with the update (Cure53 CDM-02-010).
+func (p *Server) applyRequestedMCPServerIDs(ctx context.Context, store database.Store, lockedChat database.Chat, requested *[]uuid.UUID) (database.Chat, error) {
+ if requested == nil {
+ return lockedChat, nil
+ }
+ if isExploreSubagentMode(lockedChat.Mode) {
+ p.logger.Warn(ctx,
+ "ignoring explore subagent mcp server ids update, snapshot is immutable after spawn",
+ slog.F("chat_id", lockedChat.ID),
+ )
+ return lockedChat, nil
+ }
+ enforcedIDs, err := enforceForcedMCPServerIDs(ctx, store, lockedChat.OrganizationID, lockedChat.OwnerID, *requested)
+ if err != nil {
+ return database.Chat{}, err
+ }
+ updated, err := store.UpdateChatMCPServerIDs(ctx, database.UpdateChatMCPServerIDsParams{
+ ID: lockedChat.ID,
+ MCPServerIDs: enforcedIDs,
+ })
+ if err != nil {
+ return database.Chat{}, xerrors.Errorf("update chat mcp server ids: %w", err)
+ }
+ return updated, nil
+}
+
// CreateChat creates a chat with its initial history through
// chatstate.CreateChat. The new chat starts in `running` status per
// the chat execution state model. Ownership hints wake chat workers.
@@ -1513,30 +1547,9 @@ func (p *Server) SendMessage(
return err
}
- // Update MCP server IDs on the chat when explicitly provided.
- // Explore child chats keep the spawn-time snapshot immutable.
- if requestedMCPServerIDs != nil {
- if isExploreSubagentMode(lockedChat.Mode) {
- p.logger.Warn(ctx,
- "ignoring explore subagent mcp server ids update, snapshot is immutable after spawn",
- slog.F("chat_id", opts.ChatID),
- )
- } else {
- // Force On MCP servers are enforced server-side so a
- // caller cannot remove them by tampering with the
- // update (Cure53 CDM-02-010).
- enforcedIDs, enforceErr := enforceForcedMCPServerIDs(ctx, store, lockedChat.OrganizationID, lockedChat.OwnerID, *requestedMCPServerIDs)
- if enforceErr != nil {
- return enforceErr
- }
- lockedChat, err = store.UpdateChatMCPServerIDs(ctx, database.UpdateChatMCPServerIDsParams{
- ID: opts.ChatID,
- MCPServerIDs: enforcedIDs,
- })
- if err != nil {
- return xerrors.Errorf("update chat mcp server ids: %w", err)
- }
- }
+ lockedChat, err = p.applyRequestedMCPServerIDs(ctx, store, lockedChat, requestedMCPServerIDs)
+ if err != nil {
+ return err
}
messageCreatedBy := opts.CreatedBy
@@ -1884,6 +1897,11 @@ func (p *Server) EditMessage(
}
editedMsg = target
+ lockedChat, err = p.applyRequestedMCPServerIDs(ctx, store, lockedChat, opts.MCPServerIDs)
+ if err != nil {
+ return err
+ }
+
modelOverride, err := validateModelConfigOverride(ctx, store, lockedChat.OrganizationID, opts.ModelConfigID)
if err != nil {
return err
diff --git a/coderd/x/chatd/chatd_test.go b/coderd/x/chatd/chatd_test.go
index 0e4385ca625..27de4c9fce6 100644
--- a/coderd/x/chatd/chatd_test.go
+++ b/coderd/x/chatd/chatd_test.go
@@ -2209,6 +2209,147 @@ func TestEditMessageRejectsNonUserMessage(t *testing.T) {
require.True(t, errors.Is(err, chatd.ErrEditedMessageNotUser))
}
+func TestEditMessage_MCPServerIDs(t *testing.T) {
+ t.Parallel()
+
+ db, ps := dbtestutil.NewDB(t)
+ replica := newTestServer(t, db, ps, uuid.New())
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ user, org, model := seedChatDependencies(t, db)
+
+ optionalConfig := dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
+ OrganizationID: org.ID,
+ DisplayName: "Optional MCP",
+ Slug: "optional-mcp",
+ CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true},
+ UpdatedBy: uuid.NullUUID{UUID: user.ID, Valid: true},
+ })
+
+ chat, err := replica.CreateChat(ctx, chatd.CreateOptions{
+ OrganizationID: org.ID,
+ OwnerID: user.ID,
+ Title: "edit-mcp-server-ids",
+ ModelConfigID: model.ID,
+ MCPServerIDs: []uuid.UUID{},
+ InitialUserContent: []codersdk.ChatMessagePart{codersdk.ChatMessageText("hello")},
+ })
+ require.NoError(t, err)
+
+ latestUserMessageID := func(chatID uuid.UUID) int64 {
+ messages, err := db.GetChatMessagesByChatID(ctx, database.GetChatMessagesByChatIDParams{
+ ChatID: chatID,
+ AfterID: 0,
+ })
+ require.NoError(t, err)
+ var id int64
+ for _, message := range messages {
+ if message.Role == database.ChatMessageRoleUser && !message.Deleted {
+ id = message.ID
+ }
+ }
+ require.NotZero(t, id)
+ return id
+ }
+
+ // An edit that provides MCP server IDs replaces the selection.
+ _, err = replica.EditMessage(ctx, chatd.EditMessageOptions{
+ ChatID: chat.ID,
+ CreatedBy: user.ID,
+ EditedMessageID: latestUserMessageID(chat.ID),
+ Content: []codersdk.ChatMessagePart{codersdk.ChatMessageText("edit with mcp")},
+ MCPServerIDs: &[]uuid.UUID{optionalConfig.ID},
+ })
+ require.NoError(t, err)
+
+ dbChat, err := db.GetChatByID(ctx, chat.ID)
+ require.NoError(t, err)
+ require.ElementsMatch(t, []uuid.UUID{optionalConfig.ID}, dbChat.MCPServerIDs)
+
+ // A nil selection preserves the persisted one.
+ _, err = replica.EditMessage(ctx, chatd.EditMessageOptions{
+ ChatID: chat.ID,
+ CreatedBy: user.ID,
+ EditedMessageID: latestUserMessageID(chat.ID),
+ Content: []codersdk.ChatMessagePart{codersdk.ChatMessageText("edit without mcp")},
+ })
+ require.NoError(t, err)
+
+ dbChat, err = db.GetChatByID(ctx, chat.ID)
+ require.NoError(t, err)
+ require.ElementsMatch(t, []uuid.UUID{optionalConfig.ID}, dbChat.MCPServerIDs)
+
+ // An edit that clears the list cannot remove a force_on server
+ // (Cure53 CDM-02-010 parity with the send path).
+ forcedConfig := dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
+ OrganizationID: org.ID,
+ DisplayName: "Forced MCP",
+ Slug: "forced-mcp",
+ Availability: "force_on",
+ CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true},
+ UpdatedBy: uuid.NullUUID{UUID: user.ID, Valid: true},
+ })
+ _, err = replica.EditMessage(ctx, chatd.EditMessageOptions{
+ ChatID: chat.ID,
+ CreatedBy: user.ID,
+ EditedMessageID: latestUserMessageID(chat.ID),
+ Content: []codersdk.ChatMessagePart{codersdk.ChatMessageText("edit clearing mcp")},
+ MCPServerIDs: &[]uuid.UUID{},
+ })
+ require.NoError(t, err)
+
+ dbChat, err = db.GetChatByID(ctx, chat.ID)
+ require.NoError(t, err)
+ require.ElementsMatch(t, []uuid.UUID{forcedConfig.ID}, dbChat.MCPServerIDs,
+ "force_on MCP server must survive an emptied mcp_server_ids edit")
+
+ // Explore child chats keep the spawn-time MCP snapshot immutable.
+ exploreContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{
+ codersdk.ChatMessageText("explore"),
+ })
+ require.NoError(t, err)
+ createdExplore, err := chatstate.CreateChat(ctx, db, ps, chatstate.CreateChatInput{
+ OrganizationID: org.ID,
+ OwnerID: user.ID,
+ ParentChatID: uuid.NullUUID{UUID: chat.ID, Valid: true},
+ RootChatID: uuid.NullUUID{UUID: chat.ID, Valid: true},
+ LastModelConfigID: model.ID,
+ Title: "explore-mcp-immutable",
+ Mode: database.NullChatMode{
+ ChatMode: database.ChatModeExplore,
+ Valid: true,
+ },
+ MCPServerIDs: []uuid.UUID{optionalConfig.ID},
+ ClientType: database.ChatClientTypeApi,
+ InitialMessages: []chatstate.Message{
+ {
+ Role: database.ChatMessageRoleUser,
+ Content: exploreContent,
+ Visibility: database.ChatMessageVisibilityBoth,
+ ContentVersion: chatprompt.CurrentContentVersion,
+ CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true},
+ ModelConfigID: uuid.NullUUID{UUID: model.ID, Valid: true},
+ },
+ },
+ })
+ require.NoError(t, err)
+ exploreChat := createdExplore.Chat
+
+ _, err = replica.EditMessage(ctx, chatd.EditMessageOptions{
+ ChatID: exploreChat.ID,
+ CreatedBy: user.ID,
+ EditedMessageID: latestUserMessageID(exploreChat.ID),
+ Content: []codersdk.ChatMessagePart{codersdk.ChatMessageText("edit explore")},
+ MCPServerIDs: &[]uuid.UUID{},
+ })
+ require.NoError(t, err)
+
+ dbChat, err = db.GetChatByID(ctx, exploreChat.ID)
+ require.NoError(t, err)
+ require.ElementsMatch(t, []uuid.UUID{optionalConfig.ID}, dbChat.MCPServerIDs,
+ "explore subagent MCP snapshot must be immutable after spawn")
+}
+
// TestEditMessageDebugCleanupDeletesPreEditRuns verifies that
// EditMessage schedules the chat debug cleanup goroutine when debug
// logging is enabled and that it deletes debug runs tied to the
diff --git a/codersdk/chats.go b/codersdk/chats.go
index 5718eba10a4..b1c05172b72 100644
--- a/codersdk/chats.go
+++ b/codersdk/chats.go
@@ -645,6 +645,10 @@ type EditChatMessageRequest struct {
// When nil the original message's model is preserved.
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
+ // MCPServerIDs, when set, replaces the chat's MCP server selection
+ // before the replacement turn runs. When nil the current selection
+ // is preserved.
+ MCPServerIDs *[]uuid.UUID `json:"mcp_server_ids,omitempty" format:"uuid"`
}
// CreateChatMessageResponse is the response from adding a message to a chat.
diff --git a/docs/reference/api/chats.md b/docs/reference/api/chats.md
index 64493ec1393..4c780792494 100644
--- a/docs/reference/api/chats.md
+++ b/docs/reference/api/chats.md
@@ -2122,6 +2122,9 @@ Experimental: this endpoint is subject to change.
"type": "text"
}
],
+ "mcp_server_ids": [
+ "497f6eca-6276-4993-bfeb-53cbbbba6f08"
+ ],
"model_config_id": "f5fb4d91-62ca-4377-9ee6-5d43ba00d205",
"reasoning_effort": "string"
}
diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md
index 4e2dbb5361e..f517c9b79d3 100644
--- a/docs/reference/api/schemas.md
+++ b/docs/reference/api/schemas.md
@@ -8680,6 +8680,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"type": "text"
}
],
+ "mcp_server_ids": [
+ "497f6eca-6276-4993-bfeb-53cbbbba6f08"
+ ],
"model_config_id": "f5fb4d91-62ca-4377-9ee6-5d43ba00d205",
"reasoning_effort": "string"
}
@@ -8690,6 +8693,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| Name | Type | Required | Restrictions | Description |
|--------------------|-----------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `content` | array of [codersdk.ChatInputPart](#codersdkchatinputpart) | false | | |
+| `mcp_server_ids` | array of string | false | | Mcp server ids when set, replaces the chat's MCP server selection before the replacement turn runs. When nil the current selection is preserved. |
| `model_config_id` | string | false | | Model config ID when set, overrides the model used for the replacement user message and the assistant turn that follows. When nil the original message's model is preserved. |
| `reasoning_effort` | string | false | | |
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index c8405b14923..7ebd2042386 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -4997,6 +4997,12 @@ export interface EditChatMessageRequest {
*/
readonly model_config_id?: string;
readonly reasoning_effort?: string;
+ /**
+ * MCPServerIDs, when set, replaces the chat's MCP server selection
+ * before the replacement turn runs. When nil the current selection
+ * is preserved.
+ */
+ readonly mcp_server_ids?: string[];
}
// From codersdk/chats.go
diff --git a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
index dcdc77ced22..1969798f382 100644
--- a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
+++ b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
@@ -3735,6 +3735,79 @@ export const RemoveLastMCPServer: Story = {
},
};
+const mcpEditableUserMessage: TypesGen.ChatMessage = {
+ ...MockChatMessage,
+ id: 5,
+ chat_id: CHAT_ID,
+ content: [{ type: "text", text: "Edit this request" }],
+};
+
+/**
+ * An MCP server toggled on while editing a message must ride along in
+ * the edit request.
+ */
+export const EditAppliesMCPServerSelection: Story = {
+ parameters: {
+ queries: buildQueries(
+ {
+ id: CHAT_ID,
+ ...baseChatFields,
+ title: "Edit applies MCP selection",
+ status: "waiting",
+ mcp_server_ids: [],
+ },
+ {
+ messages: [mcpEditableUserMessage],
+ queued_messages: [],
+ has_more: false,
+ },
+ {
+ diffUrl: undefined,
+ mcpServers: [MockMCPServerConfig],
+ },
+ ),
+ },
+ beforeEach: () => {
+ spyOn(API.experimental, "getUserSkills").mockResolvedValue([]);
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const body = within(document.body);
+ const editSpy = spyOn(
+ API.experimental,
+ "editChatMessage",
+ ).mockResolvedValue({
+ message: { ...mcpEditableUserMessage, id: 6 },
+ });
+
+ await userEvent.click(
+ await canvas.findByRole("button", { name: "Edit message" }),
+ );
+ await userEvent.click(canvas.getByRole("button", { name: "More options" }));
+ await userEvent.click(
+ await body.findByRole("switch", {
+ name: `Enable ${MockMCPServerConfig.display_name}`,
+ }),
+ );
+ // Close the plus menu via its trigger; Escape would exit edit mode.
+ await userEvent.click(canvas.getByRole("button", { name: "More options" }));
+ await userEvent.click(
+ await canvas.findByRole("button", { name: "Save Edit" }),
+ );
+
+ await waitFor(() => {
+ expect(editSpy).toHaveBeenCalledTimes(1);
+ });
+ expect(editSpy).toHaveBeenCalledWith(
+ CHAT_ID,
+ 5,
+ expect.objectContaining({
+ mcp_server_ids: [MockMCPServerConfig.id],
+ }),
+ );
+ },
+};
+
/**
* The send flow renders the durable user row once the server accepts the
* prompt, before the assistant turn produces any output.
diff --git a/site/src/pages/AgentsPage/AgentChatPage.tsx b/site/src/pages/AgentsPage/AgentChatPage.tsx
index 0adeda7a70a..40ee653b18b 100644
--- a/site/src/pages/AgentsPage/AgentChatPage.tsx
+++ b/site/src/pages/AgentsPage/AgentChatPage.tsx
@@ -1752,6 +1752,7 @@ const AgentChatPage: FC = () => {
reasoning_effort: isEditReasoningEffortDirtyRef.current
? effectiveReasoningEffort
: undefined,
+ mcp_server_ids: [...effectiveMCPServerIds],
};
const optimisticMessage = originalEditedMessage
? buildOptimisticEditedMessage({
From 2dbe792d6b7c6b7050e74d9326b49aa34bdd3e8d Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Mon, 24 Aug 2026 18:52:54 +0200
Subject: [PATCH 03/10] refactor: consolidate viewport hooks and remove
defineProperty matchMedia stub (#28460)
Follow-up to review feedback on #28387.
That PR added three hooks for one feature (`useIsBelowLgViewport`,
`useIsBelowMdViewport`, and the local single-use
`useRightPanelNarrowSuppression`) and stubbed `window.matchMedia` in
tests with `Object.defineProperty`.
- Replace the two single-purpose viewport hooks with one generic
`useMediaQuery(query)`. Callers pass the shared Tailwind-aligned query
constants from `utils/mobile.ts`. The one new hook replaces the two
deleted ones (net -1).
- Inline `useRightPanelNarrowSuppression` into `AgentChatPage`, its only
consumer, and drop its `renderHook` unit suite. The behavior stays
covered by the narrow-viewport stories; the widening-restore case moved
into the `NarrowingSuppressesExpandedPanel` play function.
- Rework `testHelpers/matchMedia.ts` to install the stub with `spyOn`
from `storybook/test` instead of `Object.defineProperty`. The helper is
story-only now (stories run in real Chromium; jsdom has no
`matchMedia`), so any future unit test needing a stub should use
`vi.stubGlobal` directly.
- Encode the feedback in the canonical FE contract so it gets caught
during development: `.claude/docs/FRONTEND_PATTERNS.md` now bans new
React hooks when an existing hook, a plain function, or component state
suffices (FE3) and bans replacing browser globals with
`Object.defineProperty` in tests or stories (FE9: `vi.stubGlobal` /
`spyOn`), and notes that `renderHook` suites for stateful UI hooks
belong in the consuming component's story (FE1). The `frontend-review`
skill checklist flags all three. `site/AGENTS.md` is unchanged since it
already defers to the patterns doc.
Validation: `pnpm check`, `pnpm format:check`, `pnpm lint` (biome,
types, knip, circular deps, compiler check), `AgentChatPage.test.ts` (70
passed), story runs for `AgentChatPage.stories.tsx` and
`WorkspacePill.stories.tsx` (49 passed in Chromium).
> Xum acted on Mike's behalf (@ibetitsmike).
(cherry picked from commit 3c3240836efb5cb17aaef190dae11a5e2da7a52d)
---
.claude/docs/FRONTEND_PATTERNS.md | 8 +++
.claude/skills/frontend-review/SKILL.md | 13 +++--
site/src/hooks/useIsBelowLgViewport.ts | 14 -----
site/src/hooks/useIsBelowMdViewport.ts | 14 -----
site/src/hooks/useMediaQuery.ts | 22 ++++++++
.../AgentsPage/AgentChatPage.stories.tsx | 7 +++
.../pages/AgentsPage/AgentChatPage.test.ts | 51 -------------------
site/src/pages/AgentsPage/AgentChatPage.tsx | 46 +++++++----------
.../AgentsPage/components/WorkspacePill.tsx | 5 +-
site/src/testHelpers/matchMedia.ts | 37 ++++++--------
site/src/utils/mobile.ts | 28 ++--------
11 files changed, 89 insertions(+), 156 deletions(-)
delete mode 100644 site/src/hooks/useIsBelowLgViewport.ts
delete mode 100644 site/src/hooks/useIsBelowMdViewport.ts
create mode 100644 site/src/hooks/useMediaQuery.ts
diff --git a/.claude/docs/FRONTEND_PATTERNS.md b/.claude/docs/FRONTEND_PATTERNS.md
index 1a52ae555db..360a2c4d438 100644
--- a/.claude/docs/FRONTEND_PATTERNS.md
+++ b/.claude/docs/FRONTEND_PATTERNS.md
@@ -30,6 +30,9 @@ function actually exercises the interaction. Jest/RTL tests are for pure logic
- When a component depends on the current time or date, accept it as a prop or
via context instead of reading `new Date()` or `Date.now()` internally, so
stories render deterministically without mocking globals.
+- `renderHook` suites for stateful UI hooks are interaction tests, not pure
+ logic. Cover that behavior through the story of the component that uses the
+ hook.
**Incorrect (interaction test in Jest/RTL):**
@@ -89,6 +92,8 @@ const config: ChatModel = parseConfig(data);
of existing ones.
- Use existing wrapped primitives (Combobox, dialogs, tables) instead of
hand-assembling the underlying pieces they already wrap.
+- Do not introduce a new React hook when an existing hook, a plain function,
+ or component state can express the logic.
- Delete dead code and unreachable branches instead of carrying them along.
- Keep the PR scoped to one change. Move unrelated cleanups, renames, and
drive-by refactors to separate PRs.
@@ -209,6 +214,9 @@ Decide where logic goes before reaching for `useEffect`:
is readable on its own. Share the entity fixture, not a pre-wired query
object.
- Query keys in mocks follow FE7: import the constant.
+- Never replace browser globals with `Object.defineProperty` in tests or
+ stories. Use `vi.stubGlobal` in unit tests and `spyOn` from
+ `storybook/test` in stories.
## FE10: Tests assert observable behavior
diff --git a/.claude/skills/frontend-review/SKILL.md b/.claude/skills/frontend-review/SKILL.md
index 6da33f37b87..0d27ba52805 100644
--- a/.claude/skills/frontend-review/SKILL.md
+++ b/.claude/skills/frontend-review/SKILL.md
@@ -38,14 +38,18 @@ before they see the PR.
user-visible behavior? Then a changed or added `.stories.tsx` must exist,
and its `play` function must perform the new interaction (open the menu,
submit the form), not merely render. Interaction tests added to `.test.tsx`
- files are a FAIL unless they cover pure logic.
+ files are a FAIL unless they cover pure logic; `renderHook` suites for
+ stateful UI hooks count as interaction tests and belong in the consuming
+ component's story.
- **FE2 (types)**: Search the diff for `any`, `as unknown as`, non-null
assertions in any form (`x!.y`, `items[0]!`, `fn()!`, `value! as T`), and
new `as` casts. Check that API data uses types from `api/typesGenerated.ts`.
- **FE3 (reuse/scope)**: For each new component, hook, or helper, search
`site/src/components/` and sibling folders for an existing equivalent.
Flag near-duplicates, hand-assembled versions of wrapped primitives, dead
- branches, and unrelated changes bundled into the diff.
+ branches, and unrelated changes bundled into the diff. Flag new React hooks
+ that an existing hook, a plain function, or component state could replace;
+ several new single-use hooks in one diff is a FAIL.
- **FE4 (comments)**: Read every comment line the diff adds or edits. Flag
any comment that restates the identifier, assertion, or control flow.
Verify surviving comments are factually correct.
@@ -68,7 +72,10 @@ before they see the PR.
reads.
- **FE9 (fixtures)**: Flag inline entity literals that duplicate or deviate
from `Mock*` fixtures in `site/src/testHelpers/`, and shared pre-wired
- query objects instead of per-story inline `{ key, data }` wiring.
+ query objects instead of per-story inline `{ key, data }` wiring. Flag any
+ `Object.defineProperty` replacement of a browser global in tests or
+ stories: unit tests stub with `vi.stubGlobal`, stories mock existing
+ globals with `spyOn` from `storybook/test`.
- **FE10 (test queries)**: Flag `querySelector`, class-name substring
matches, geometry assertions, `behavior: "smooth"` dependence, and
locale-less `toLocaleString()` in changed tests and stories.
diff --git a/site/src/hooks/useIsBelowLgViewport.ts b/site/src/hooks/useIsBelowLgViewport.ts
deleted file mode 100644
index 389176ef857..00000000000
--- a/site/src/hooks/useIsBelowLgViewport.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { useSyncExternalStore } from "react";
-import {
- belowLgViewportMediaQuery,
- createMediaQuerySubscribe,
- isBelowLgViewport,
-} from "#/utils/mobile";
-
-const subscribeBelowLgViewport = createMediaQuerySubscribe(
- belowLgViewportMediaQuery,
-);
-
-export const useIsBelowLgViewport = (): boolean => {
- return useSyncExternalStore(subscribeBelowLgViewport, isBelowLgViewport);
-};
diff --git a/site/src/hooks/useIsBelowMdViewport.ts b/site/src/hooks/useIsBelowMdViewport.ts
deleted file mode 100644
index ab7642b2d37..00000000000
--- a/site/src/hooks/useIsBelowMdViewport.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { useSyncExternalStore } from "react";
-import {
- belowMdViewportMediaQuery,
- createMediaQuerySubscribe,
- isBelowMdViewport,
-} from "#/utils/mobile";
-
-const subscribeBelowMdViewport = createMediaQuerySubscribe(
- belowMdViewportMediaQuery,
-);
-
-export const useIsBelowMdViewport = (): boolean => {
- return useSyncExternalStore(subscribeBelowMdViewport, isBelowMdViewport);
-};
diff --git a/site/src/hooks/useMediaQuery.ts b/site/src/hooks/useMediaQuery.ts
new file mode 100644
index 00000000000..b99b83036c2
--- /dev/null
+++ b/site/src/hooks/useMediaQuery.ts
@@ -0,0 +1,22 @@
+import { useCallback, useSyncExternalStore } from "react";
+
+/**
+ * Subscribes to a CSS media query and returns whether it currently
+ * matches, re-rendering on change. Pass a shared query constant from
+ * `utils/mobile.ts` so breakpoints stay aligned with Tailwind
+ * utilities.
+ */
+export const useMediaQuery = (query: string): boolean => {
+ const subscribe = useCallback(
+ (onStoreChange: () => void) => {
+ const mediaQuery = window.matchMedia(query);
+ mediaQuery.addEventListener("change", onStoreChange);
+ return () => mediaQuery.removeEventListener("change", onStoreChange);
+ },
+ [query],
+ );
+ return useSyncExternalStore(
+ subscribe,
+ () => window.matchMedia(query).matches,
+ );
+};
diff --git a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
index 1969798f382..c6836456f7f 100644
--- a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
+++ b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx
@@ -1939,6 +1939,13 @@ export const NarrowingSuppressesExpandedPanel: Story = {
expect(
canvas.queryByRole("tab", { name: "Summary" }),
).not.toBeInTheDocument();
+
+ // Widening again restores the persisted panel, still expanded.
+ narrowingMedia?.setMatches(belowLgViewportMediaQuery, false);
+ await waitFor(() => {
+ expect(canvas.getByRole("tab", { name: "Summary" })).toBeVisible();
+ });
+ expect(messagesRegion.checkVisibility()).toBe(false);
},
};
diff --git a/site/src/pages/AgentsPage/AgentChatPage.test.ts b/site/src/pages/AgentsPage/AgentChatPage.test.ts
index 82b9503090d..ca2222ccc17 100644
--- a/site/src/pages/AgentsPage/AgentChatPage.test.ts
+++ b/site/src/pages/AgentsPage/AgentChatPage.test.ts
@@ -18,7 +18,6 @@ import {
MockWorkspaceAgent,
MockWorkspaceApp,
} from "#/testHelpers/entities";
-import { setupMatchMedia } from "#/testHelpers/matchMedia";
import {
buildInactiveChatQueueReconciliation,
draftInputStorageKeyPrefix,
@@ -32,7 +31,6 @@ import {
settlePromotedQueueHead,
submitEdit,
useConversationEditingState,
- useRightPanelNarrowSuppression,
waitForPendingChatSettingsSyncs,
} from "./AgentChatPage";
import type { ChatMessageInputRef } from "./components/AgentChatInput";
@@ -1407,52 +1405,3 @@ describe("isChatAgentBindingUnresolved", () => {
);
});
});
-
-describe("useRightPanelNarrowSuppression", () => {
- const belowLgQuery = "(max-width: 1023px)";
-
- const setupBelowLg = (initialBelowLg: boolean) => {
- const media = setupMatchMedia({ [belowLgQuery]: initialBelowLg });
- return {
- setBelowLg: (value: boolean) => media.setMatches(belowLgQuery, value),
- };
- };
-
- it("suppresses the panel when mounted below the lg breakpoint", () => {
- setupBelowLg(true);
- const { result } = renderHook(() => useRightPanelNarrowSuppression());
- expect(result.current.suppressed).toBe(true);
- });
-
- it("does not suppress the panel when mounted at or above lg", () => {
- setupBelowLg(false);
- const { result } = renderHook(() => useRightPanelNarrowSuppression());
- expect(result.current.suppressed).toBe(false);
- });
-
- it("suppresses on narrowing and clears on widening", () => {
- const media = setupBelowLg(false);
- const { result } = renderHook(() => useRightPanelNarrowSuppression());
-
- act(() => media.setBelowLg(true));
- expect(result.current.suppressed).toBe(true);
-
- act(() => media.setBelowLg(false));
- expect(result.current.suppressed).toBe(false);
- });
-
- it("stays cleared after an explicit clearSuppression until the next narrowing", () => {
- const media = setupBelowLg(false);
- const { result } = renderHook(() => useRightPanelNarrowSuppression());
-
- act(() => media.setBelowLg(true));
- expect(result.current.suppressed).toBe(true);
-
- act(() => result.current.clearSuppression());
- expect(result.current.suppressed).toBe(false);
-
- act(() => media.setBelowLg(false));
- act(() => media.setBelowLg(true));
- expect(result.current.suppressed).toBe(true);
- });
-});
diff --git a/site/src/pages/AgentsPage/AgentChatPage.tsx b/site/src/pages/AgentsPage/AgentChatPage.tsx
index 40ee653b18b..4e7beeac569 100644
--- a/site/src/pages/AgentsPage/AgentChatPage.tsx
+++ b/site/src/pages/AgentsPage/AgentChatPage.tsx
@@ -65,12 +65,12 @@ import type { ChatMessagePart } from "#/api/typesGenerated";
import { useProxy } from "#/contexts/ProxyContext";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useAIGatewayEnabled } from "#/hooks/useEmbeddedMetadata";
-import { useIsBelowLgViewport } from "#/hooks/useIsBelowLgViewport";
+import { useMediaQuery } from "#/hooks/useMediaQuery";
import {
getDefaultOrganizationName,
useDashboard,
} from "#/modules/dashboard/useDashboard";
-import { isMobileViewport } from "#/utils/mobile";
+import { belowLgViewportMediaQuery, isMobileViewport } from "#/utils/mobile";
import { pageTitle } from "#/utils/page";
import { rewriteLocalhostURL } from "#/utils/portForward";
import { createReconnectingWebSocket } from "#/utils/reconnectingWebSocket";
@@ -134,29 +134,6 @@ import {
/** localStorage key controlling whether the right panel is visible. */
export const RIGHT_PANEL_OPEN_KEY = "agents.right-panel-open";
-/**
- * Below the `lg` breakpoint, chat and the right panel are mutually
- * exclusive, so a panel left open on a wide window would hide chat as
- * soon as the window narrows. This suppresses the panel while narrow
- * without touching the persisted preference: widening restores the
- * panel, and an explicit user action (clearSuppression) overrides it.
- */
-export function useRightPanelNarrowSuppression(): {
- suppressed: boolean;
- clearSuppression: () => void;
-} {
- const isBelowLg = useIsBelowLgViewport();
- const [suppressed, setSuppressed] = useState(isBelowLg);
- const [prevIsBelowLg, setPrevIsBelowLg] = useState(isBelowLg);
- // Render-time state adjustment on breakpoint crossings; see
- // https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
- if (isBelowLg !== prevIsBelowLg) {
- setPrevIsBelowLg(isBelowLg);
- setSuppressed(isBelowLg);
- }
- return { suppressed, clearSuppression: () => setSuppressed(false) };
-}
-
const lastModelConfigIDStorageKey = "agents.last-model-config-id";
const AGENT_BINDING_REPAIR_POLL_MS = 30_000;
@@ -886,15 +863,28 @@ const AgentChatPage: FC = () => {
const [sidebarPanelPreference, setSidebarPanelPreference] = useState(() => {
return localStorage.getItem(RIGHT_PANEL_OPEN_KEY) === "true";
});
- const { suppressed: panelSuppressedOnNarrow, clearSuppression } =
- useRightPanelNarrowSuppression();
+ // Below the lg breakpoint, chat and the right panel are mutually
+ // exclusive, so a panel left open on a wide window would hide chat
+ // as soon as the window narrows. Suppression hides the panel while
+ // narrow without touching the persisted preference: widening
+ // restores the panel, and an explicit toggle overrides it.
+ const isBelowLg = useMediaQuery(belowLgViewportMediaQuery);
+ const [panelSuppressedOnNarrow, setPanelSuppressedOnNarrow] =
+ useState(isBelowLg);
+ const [prevIsBelowLg, setPrevIsBelowLg] = useState(isBelowLg);
+ // Render-time state adjustment on breakpoint crossings; see
+ // https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
+ if (isBelowLg !== prevIsBelowLg) {
+ setPrevIsBelowLg(isBelowLg);
+ setPanelSuppressedOnNarrow(isBelowLg);
+ }
// Canonical panel visibility: the persisted preference gated by the
// narrow-viewport suppression. Only this derived value may be
// rendered or handed to children; the raw preference stays local.
const showSidebarPanel = sidebarPanelPreference && !panelSuppressedOnNarrow;
const handleSetShowSidebarPanel = (next: boolean) => {
- clearSuppression();
+ setPanelSuppressedOnNarrow(false);
setSidebarPanelPreference(next);
localStorage.setItem(RIGHT_PANEL_OPEN_KEY, String(next));
};
diff --git a/site/src/pages/AgentsPage/components/WorkspacePill.tsx b/site/src/pages/AgentsPage/components/WorkspacePill.tsx
index 04e5dff8469..c72851e8835 100644
--- a/site/src/pages/AgentsPage/components/WorkspacePill.tsx
+++ b/site/src/pages/AgentsPage/components/WorkspacePill.tsx
@@ -35,7 +35,7 @@ import {
} from "#/components/Tooltip/Tooltip";
import { useProxy } from "#/contexts/ProxyContext";
import { useClipboard } from "#/hooks/useClipboard";
-import { useIsBelowMdViewport } from "#/hooks/useIsBelowMdViewport";
+import { useMediaQuery } from "#/hooks/useMediaQuery";
import {
getTerminalHref,
getVSCodeHref,
@@ -47,6 +47,7 @@ import {
usePortsData,
} from "#/modules/resources/usePortsData";
import { cn } from "#/utils/cn";
+import { belowMdViewportMediaQuery } from "#/utils/mobile";
import { getWorkspaceStatus, StatusIcon } from "./StatusIcon";
import { MobilePortsPanel, PortsMenuItem } from "./WorkspacePillPorts";
@@ -98,7 +99,7 @@ export const WorkspacePill: FC = ({
// Flyout sub-menus clip on mobile.
const [view, setView] = useState<"main" | "ports">("main");
const [focusPortsOnMain, setFocusPortsOnMain] = useState(false);
- const isBelowMd = useIsBelowMdViewport();
+ const isBelowMd = useMediaQuery(belowMdViewportMediaQuery);
const showPortsView = view === "ports" && isBelowMd;
const portsData = usePortsData(
diff --git a/site/src/testHelpers/matchMedia.ts b/site/src/testHelpers/matchMedia.ts
index 026a6dc8545..16029569460 100644
--- a/site/src/testHelpers/matchMedia.ts
+++ b/site/src/testHelpers/matchMedia.ts
@@ -1,11 +1,16 @@
+import { spyOn } from "storybook/test";
+
/**
- * Replaces `window.matchMedia` with a controllable stub for tests and
- * stories. Queries listed in `initialMatches` report their configured
- * value; every other query delegates to the real `matchMedia` (or
- * reports `false` where none exists, e.g. jsdom) so unrelated
+ * Replaces `window.matchMedia` with a controllable stub for stories.
+ * Queries listed in `initialMatches` report their configured value;
+ * every other query delegates to the real `matchMedia` so unrelated
* responsive components keep behaving truthfully. `setMatches` updates
* a query and notifies its registered change listeners; `restore` puts
* the original `window.matchMedia` back.
+ *
+ * Story-only: stories run in a real browser, so a real `matchMedia` to
+ * delegate to always exists. jsdom has no `matchMedia`, so unit tests
+ * must install their own stub with `vi.stubGlobal` instead.
*/
export const setupMatchMedia = (initialMatches: Record) => {
const matches = { ...initialMatches };
@@ -18,15 +23,11 @@ export const setupMatchMedia = (initialMatches: Record) => {
}
return set;
};
- const original = window.matchMedia;
- const originalFn =
- typeof original === "function" ? original.bind(window) : undefined;
- Object.defineProperty(window, "matchMedia", {
- configurable: true,
- writable: true,
- value: (query: string): MediaQueryList => {
- if (!(query in matches) && originalFn) {
- return originalFn(query);
+ const original = window.matchMedia.bind(window);
+ const spy = spyOn(window, "matchMedia").mockImplementation(
+ (query: string): MediaQueryList => {
+ if (!(query in matches)) {
+ return original(query);
}
return {
get matches() {
@@ -51,7 +52,7 @@ export const setupMatchMedia = (initialMatches: Record) => {
removeListener: () => {},
} satisfies MediaQueryList;
},
- });
+ );
return {
setMatches: (query: string, value: boolean) => {
matches[query] = value;
@@ -64,12 +65,6 @@ export const setupMatchMedia = (initialMatches: Record) => {
}
}
},
- restore: () => {
- Object.defineProperty(window, "matchMedia", {
- configurable: true,
- writable: true,
- value: original,
- });
- },
+ restore: () => spy.mockRestore(),
};
};
diff --git a/site/src/utils/mobile.ts b/site/src/utils/mobile.ts
index c8b80c4fd84..05607b9db4d 100644
--- a/site/src/utils/mobile.ts
+++ b/site/src/utils/mobile.ts
@@ -8,19 +8,6 @@ export const isMobileViewport = (): boolean => {
return window.matchMedia("(max-width: 639px)").matches;
};
-/**
- * Builds a `useSyncExternalStore` subscribe function that notifies on
- * changes to the given media query, so every viewport hook shares one
- * listener lifecycle implementation.
- */
-export const createMediaQuerySubscribe =
- (query: string) =>
- (onStoreChange: () => void): (() => void) => {
- const mediaQuery = window.matchMedia(query);
- mediaQuery.addEventListener("change", onStoreChange);
- return () => mediaQuery.removeEventListener("change", onStoreChange);
- };
-
export const belowMdViewportMediaQuery = "(max-width: 767px)";
/**
@@ -35,15 +22,10 @@ export const isBelowMdViewport = (): boolean => {
return window.matchMedia(belowMdViewportMediaQuery).matches;
};
-export const belowLgViewportMediaQuery = "(max-width: 1023px)";
-
/**
- * Returns `true` when the viewport width is below the `lg` Tailwind
- * breakpoint (< 1024 px). Use this to align with `lg:` Tailwind
- * utilities that switch between a side-by-side layout and a
- * single-panel-at-a-time layout (e.g. the Agents chat page's chat vs.
- * right panel split).
+ * Matches viewports below the `lg` Tailwind breakpoint (< 1024 px),
+ * aligning with `lg:` utilities that switch between a side-by-side
+ * layout and a single-panel-at-a-time layout (e.g. the Agents chat
+ * page's chat vs. right panel split).
*/
-export const isBelowLgViewport = (): boolean => {
- return window.matchMedia(belowLgViewportMediaQuery).matches;
-};
+export const belowLgViewportMediaQuery = "(max-width: 1023px)";
From cb0c11c8f6fbcfc6d62835e1e8ee4e3f1d7ac6a3 Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Tue, 25 Aug 2026 11:39:43 +0200
Subject: [PATCH 04/10] feat(coderd/x/chatd/chattool): improve find_tools
relevance and model guidance (#28476)
Broad `find_tools` queries such as "linear issues" saturated the hard
20-match cap on every call: a large server's name token-matches every
one of its tools (+8 name, +1 server), so ranking was decided by a term
carrying no discriminating information and the model got 20 activations
per search regardless of intent.
Three changes, mirroring what makes xum's `tool_catalog_search` behave
well:
- **Lower default with a `limit` argument.** Keyword matches default to
10 per call; the new optional `limit` raises that up to the existing
hard cap of 20 (non-positive values fall back to the default). Exact
`names` are explicit activation requests and bypass the limit up to the
hard cap, so naming 15 tools still activates all 15.
- **Coverage-first ranking.** Keyword matches sort by distinct query
terms matched before raw score, so a query like "linear issues" ranks
tools matching both terms above the dozens matching only the server
name.
- **Model guidance.** `queries` and `names` now carry schema
descriptions (capability keywords and what they are matched against;
exact-name activation), and the tool description opens by explaining
what deferral means, that matches become callable on the next step, and
that a `"server: terms"` prefix scopes a query to one server.
An earlier revision also inferred a server scope from unprefixed query
words ("linear issues" behaving like "linear: issues"). Review kept
surfacing edge cases in that inference, and coverage-first ranking
already resolves the original saturation complaint, so it was dropped in
favor of the explicit prefix.
The hard cap stays at 20 so the persisted result keeps fitting under the
generic tool-result truncation budget that protects activation-recovery
JSON. Budget and reservation accounting are unchanged; the frontend
renderer ignores unknown argument fields, so no `site/` change is
needed.
> [!NOTE]
> Xum acted on @ibetitsmike's behalf in this pull request.
(cherry picked from commit e244cac9c1804c58f21ea10766e859c5f68d2db3)
---
coderd/x/chatd/chattool/findtools.go | 70 ++++++++++++-------
.../chatd/chattool/findtools_internal_test.go | 48 ++++++++++++-
2 files changed, 90 insertions(+), 28 deletions(-)
diff --git a/coderd/x/chatd/chattool/findtools.go b/coderd/x/chatd/chattool/findtools.go
index 8338df54787..031a6912bbc 100644
--- a/coderd/x/chatd/chattool/findtools.go
+++ b/coderd/x/chatd/chattool/findtools.go
@@ -16,9 +16,11 @@ import (
)
const (
- FindToolsName = "find_tools"
- findToolsMaxMatches = 20
- findToolsCatalogTokens = 4000
+ FindToolsName = "find_tools"
+ // Keep broad query results concise while allowing higher explicit limits.
+ findToolsDefaultMatches = 10
+ findToolsMaxMatches = 20
+ findToolsCatalogTokens = 4000
// findToolsMaxQueries and findToolsMaxQueryTokens bound scoring
// work: queries are model output, so one call could otherwise
// carry arbitrarily many tokens scored against every entry.
@@ -83,8 +85,9 @@ type FindToolsOptions struct {
}
type FindToolsArgs struct {
- Queries []string `json:"queries,omitempty"`
- Names []string `json:"names,omitempty"`
+ Queries []string `json:"queries,omitempty" description:"Task or capability keywords, matched against tool names, descriptions, parameters, and server metadata. Prefer a few specific keywords over sentences."`
+ Names []string `json:"names,omitempty" description:"Exact cataloged tool names to activate directly."`
+ Limit int `json:"limit,omitempty" description:"Cap on total tools returned and activated per call (default 10, max 20). Exact names are always included and may exceed it."`
}
type FindToolsMatch struct {
@@ -338,15 +341,11 @@ type SearchBudget struct {
AllowFirstOverBudget bool
}
-// SearchTools includes exact name activations first, then fills the
-// remaining match slots with the top-scored keyword matches. The shared
-// cap and summary-length descriptions keep the persisted result small
-// enough that generic tool-result truncation can never corrupt the
-// activation JSON that later steps re-derive activations from. A
-// positive budget additionally skips matches whose schema weight would
-// push the aggregate past it, admitting later matches that still fit.
-// The second result counts matches skipped for budget, so callers can
-// tell an exhausted budget from no matches.
+// SearchTools prioritizes exact names, then keyword matches ranked by
+// distinct query terms matched before raw score. Exact names bypass the
+// per-call limit, but a hard cap keeps the persisted result safe from
+// generic tool-result truncation; the second return counts
+// budget-skipped matches.
func SearchTools(entries []FindToolCatalogEntry, args FindToolsArgs, budget SearchBudget) (FindToolsResult, int) {
byName := make(map[string]FindToolCatalogEntry, len(entries))
for _, entry := range entries {
@@ -360,13 +359,19 @@ func SearchTools(entries []FindToolCatalogEntry, args FindToolsArgs, budget Sear
queries := parseFindToolsQueries(entries, queryArgs)
type scoredEntry struct {
- entry FindToolCatalogEntry
- score int
+ entry FindToolCatalogEntry
+ coverage int
+ score int
+ }
+ entryTokens := make([]findToolsEntryTokens, len(entries))
+ for i, entry := range entries {
+ entryTokens[i] = tokenizeFindToolsEntry(entry)
}
scored := make([]scoredEntry, 0, len(entries))
- for _, entry := range entries {
- tokens := tokenizeFindToolsEntry(entry)
+ for i, entry := range entries {
+ tokens := entryTokens[i]
score := 0
+ matched := make(map[string]struct{})
for _, query := range queries {
if query.server != "" {
if query.exact && entry.Server != query.server {
@@ -378,31 +383,46 @@ func SearchTools(entries []FindToolCatalogEntry, args FindToolsArgs, budget Sear
}
if query.server != "" && len(query.tokens) == 0 {
score++
+ // A scope-only hit counts as one covered term; ":" cannot occur in tokens.
+ matched[":"+query.server] = struct{}{}
continue
}
for _, token := range query.tokens {
- score += tokens.score(token)
+ if tokenScore := tokens.score(token); tokenScore > 0 {
+ score += tokenScore
+ matched[token] = struct{}{}
+ }
}
}
if score > 0 {
- scored = append(scored, scoredEntry{entry: entry, score: score})
+ scored = append(scored, scoredEntry{entry: entry, coverage: len(matched), score: score})
}
}
slices.SortFunc(scored, func(a, b scoredEntry) int {
+ if a.coverage != b.coverage {
+ return b.coverage - a.coverage
+ }
if a.score != b.score {
return b.score - a.score
}
return strings.Compare(a.entry.Name, b.entry.Name)
})
+ matchLimit := args.Limit
+ if matchLimit <= 0 {
+ matchLimit = findToolsDefaultMatches
+ }
+ if matchLimit > findToolsMaxMatches {
+ matchLimit = findToolsMaxMatches
+ }
matches := make([]FindToolsMatch, 0, findToolsMaxMatches)
activatedSet := make(map[string]struct{}, findToolsMaxMatches)
usedSchemaTokens := 0.0
budgetSkipped := 0
- appendMatch := func(entry FindToolCatalogEntry) {
+ appendMatch := func(entry FindToolCatalogEntry, limit int) {
if _, exists := activatedSet[entry.Name]; exists {
return
}
- if len(matches) >= findToolsMaxMatches {
+ if len(matches) >= limit {
return
}
overBudget := budget.SchemaTokens > 0 && usedSchemaTokens+entry.SchemaTokens > budget.SchemaTokens
@@ -423,11 +443,11 @@ func SearchTools(entries []FindToolCatalogEntry, args FindToolsArgs, budget Sear
}
for _, name := range nameArgs {
if entry, ok := byName[name]; ok {
- appendMatch(entry)
+ appendMatch(entry, findToolsMaxMatches)
}
}
for _, item := range scored {
- appendMatch(item.entry)
+ appendMatch(item.entry, matchLimit)
}
activated := make([]string, 0, len(activatedSet))
for name := range activatedSet {
@@ -616,7 +636,7 @@ func (t findToolsEntryTokens) score(token string) int {
}
func buildFindToolsDescription(entries []FindToolCatalogEntry, catalogTokenBudget float64) string {
- const usage = "Search deferred MCP tools by keyword, activate exact tool names, or scope a query to one server with a \"server: terms\" prefix. Calling a cataloged tool directly by name is allowed and auto-loads its schema, but search first for unfamiliar tools. At most 20 tools are returned and activated per call; call again for more.\n\n"
+ const usage = "The MCP tools cataloged below are deferred: not in your tool list until activated. Search by keyword, activate exact tool names, or scope with a \"server: terms\" prefix; matches activate and become callable on the next step. Direct calls to cataloged tools also work, but search first for unfamiliar tools. limit caps total results per call (default 10, max 20); exact names bypass it but still spend the shared schema budget. Narrow the query or raise limit for more.\n\n"
budget := float64(findToolsCatalogTokens)
if catalogTokenBudget > 0 && catalogTokenBudget < budget {
budget = catalogTokenBudget
diff --git a/coderd/x/chatd/chattool/findtools_internal_test.go b/coderd/x/chatd/chattool/findtools_internal_test.go
index 0a7af3fdc47..b4e9b32ef98 100644
--- a/coderd/x/chatd/chattool/findtools_internal_test.go
+++ b/coderd/x/chatd/chattool/findtools_internal_test.go
@@ -50,8 +50,20 @@ func TestSearchTools(t *testing.T) {
many[i] = FindToolCatalogEntry{Name: fmt.Sprintf("server__tool_%02d", i), Description: "common"}
}
result, _ := SearchTools(many, FindToolsArgs{Queries: []string{"common"}}, SearchBudget{})
- require.Len(t, result.Matches, findToolsMaxMatches)
+ require.Len(t, result.Matches, findToolsDefaultMatches,
+ "an omitted limit returns the default match count")
require.Equal(t, "server__tool_00", result.Matches[0].Name)
+
+ raised, _ := SearchTools(many, FindToolsArgs{Queries: []string{"common"}, Limit: 15}, SearchBudget{})
+ require.Len(t, raised.Matches, 15)
+
+ clamped, _ := SearchTools(many, FindToolsArgs{Queries: []string{"common"}, Limit: 25}, SearchBudget{})
+ require.Len(t, clamped.Matches, findToolsMaxMatches,
+ "a limit above the hard cap clamps to it")
+
+ invalid, _ := SearchTools(many, FindToolsArgs{Queries: []string{"common"}, Limit: -1}, SearchBudget{})
+ require.Len(t, invalid.Matches, findToolsDefaultMatches,
+ "a non-positive limit falls back to the default")
})
t.Run("names capped and prioritized over queries", func(t *testing.T) {
t.Parallel()
@@ -62,13 +74,21 @@ func TestSearchTools(t *testing.T) {
names = append(names, many[i].Name)
}
result, _ := SearchTools(many, FindToolsArgs{Queries: []string{"common"}, Names: []string{"server__tool_24"}}, SearchBudget{})
- require.Len(t, result.Matches, findToolsMaxMatches)
+ require.Len(t, result.Matches, findToolsDefaultMatches)
require.Equal(t, "server__tool_24", result.Matches[0].Name)
require.Contains(t, result.Activated, "server__tool_24")
capped, _ := SearchTools(many, FindToolsArgs{Names: names}, SearchBudget{})
- require.Len(t, capped.Matches, findToolsMaxMatches)
+ require.Len(t, capped.Matches, findToolsMaxMatches,
+ "exact names bypass the default limit up to the hard cap")
require.Len(t, capped.Activated, findToolsMaxMatches)
+
+ bypassed, _ := SearchTools(many, FindToolsArgs{Queries: []string{"common"}, Names: names[:12], Limit: 5}, SearchBudget{})
+ require.Len(t, bypassed.Matches, 12,
+ "exact names bypass an explicit lower limit and leave no keyword slots")
+ for i, name := range names[:12] {
+ require.Equal(t, name, bypassed.Matches[i].Name)
+ }
})
t.Run("names list is bounded", func(t *testing.T) {
t.Parallel()
@@ -101,6 +121,17 @@ func TestSearchTools(t *testing.T) {
"tool description match outranks server metadata match")
require.Len(t, result.Matches, 2)
})
+ t.Run("coverage outranks concentrated score", func(t *testing.T) {
+ t.Parallel()
+ coverageEntries := []FindToolCatalogEntry{
+ {Name: "tracker__update", Description: "Assign labels and update status"},
+ {Name: "labels__tool", Description: "unrelated"},
+ }
+ result, _ := SearchTools(coverageEntries, FindToolsArgs{Queries: []string{"labels status"}}, SearchBudget{})
+ require.Len(t, result.Matches, 2)
+ require.Equal(t, "tracker__update", result.Matches[0].Name,
+ "an entry matching more distinct query terms outranks a higher single-term score")
+ })
t.Run("server prefix scope", func(t *testing.T) {
t.Parallel()
scopedEntries := []FindToolCatalogEntry{
@@ -273,6 +304,17 @@ func TestFindTools(t *testing.T) {
require.True(t, resp.IsError)
}
+func TestFindToolsArgDescriptions(t *testing.T) {
+ t.Parallel()
+ info := FindTools(FindToolsOptions{}).Info()
+ for _, name := range []string{"queries", "names", "limit"} {
+ property, ok := info.Parameters[name].(map[string]any)
+ require.True(t, ok, "parameter %q must exist in the schema", name)
+ description, _ := property["description"].(string)
+ require.NotEmpty(t, description, "parameter %q needs model guidance in its schema description", name)
+ }
+}
+
func TestFindToolsSerialToolCalls(t *testing.T) {
t.Parallel()
serial, ok := FindTools(FindToolsOptions{}).(interface{ SerialToolCalls() bool })
From 7a004237906486ae89cb57b0152cf9fb725e30c1 Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Tue, 25 Aug 2026 11:43:55 +0200
Subject: [PATCH 05/10] fix(coderd/x/chatd/mcpclient): enforce MCP connect
budget and unblock session cleanup (#28400)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Problem
During the Aug 19 dev.coder.com incident, chats stalled for up to ~15
minutes per turn. chatd reconnects to every configured MCP server on
every generation step, and one configured server
(`registry.coder.com/mcp`) was black-holing requests from the
deployment's egress IPs: TCP/requests were silently dropped, and each
connect attempt hung until the kernel gave up (~125s), far past the
nominal 10s connect budget.
The budget does not hold because go-sdk v1.7.0 detaches the context
inside `StreamableClientTransport.Connect`, and its error paths block on
HTTP work bound to that detached context. Reproduced in a test: a bare
`Connect` with a 2s deadline against a black-holed server returns after
**12s**. Step-end session cleanup (`session.Close()`) runs synchronously
in the generation loop and blocks on the same detached requests, so a
server that wedges mid-turn stalls every step boundary too.
## Changes
- **Enforce the connect budget externally** (`connectOne`): run
`Connect` + `ListTools` in a goroutine and select on the budget context.
On timeout, abandon the goroutine and leave a reaper that drains its
late result and closes any session that still materialized, so nothing
leaks and the caller returns within the budget.
- **Stop using bare `http.DefaultTransport`**: MCP traffic now rides a
cloned transport with a 5s dial timeout (converts SYN black-holes into
fast errors) and a 60s `ResponseHeaderTimeout`. `http.Client.Timeout`
stays unset so long-lived SSE streams are unaffected.
- **Close sessions in detached goroutines during cleanup**
(`ConnectAll`'s cleanup func): the sessions are discarded regardless,
and a wedged `Close` (DELETE bound to the SDK's detached context) must
not stall the generation loop at step boundaries.
One deliberate deviation from the incident plan: `ResponseHeaderTimeout`
is 60s (matching `toolCallTimeout`) instead of ~10s. The same HTTP
client serves tool-call POSTs, and a JSON-response MCP server sends no
headers until the tool finishes, so a 10s bound would falsely kill
legitimate 10-60s tools that fit today's tool-call budget. The real 10s
connect budget is enforced by the external select, not the transport.
## Tests
- Acceptance: with a black-holed server plus a healthy one configured,
`ConnectAll` returns within the budget and the healthy server's tools
are present; closing the black-holed connections makes the reaper exit
(red without the fix: 1s budget took 11s).
- A slow-but-alive server (300ms/request) still connects.
- A server that only responds after the budget: `ConnectAll` returns
promptly, the late result is reaped.
- Cleanup returns promptly while the session-teardown DELETE is wedged
server-side, and the teardown still happens in the background (red
without the fix: cleanup blocked 60s).
- Transport guard test pinning the dial/response-header bounds.
`go test ./coderd/x/chatd/...` passes (including goleak in `chatd`).
Part of the MCP connect-stall incident follow-up; observability (connect
durations in logs/debug runs) comes in a stacked follow-up PR.
> 🤖 Mux authored this PR on Mike's behalf.
(cherry picked from commit cc958b73be9d75fa040c56860c8718356a75fc5d)
---
coderd/x/chatd/mcpclient/export_test.go | 27 ++
coderd/x/chatd/mcpclient/mcpclient.go | 116 ++++++-
.../chatd/mcpclient/mcpclient_connect_test.go | 292 ++++++++++++++++++
coderd/x/chatd/mcpclient/mcphttpclient.go | 63 +++-
.../mcpclient/mcphttpclient_internal_test.go | 27 ++
5 files changed, 495 insertions(+), 30 deletions(-)
create mode 100644 coderd/x/chatd/mcpclient/mcpclient_connect_test.go
create mode 100644 coderd/x/chatd/mcpclient/mcphttpclient_internal_test.go
diff --git a/coderd/x/chatd/mcpclient/export_test.go b/coderd/x/chatd/mcpclient/export_test.go
index 50d350aba24..3bfc1ae5065 100644
--- a/coderd/x/chatd/mcpclient/export_test.go
+++ b/coderd/x/chatd/mcpclient/export_test.go
@@ -1,9 +1,36 @@
package mcpclient
+import (
+ "context"
+ "time"
+
+ "charm.land/fantasy"
+ "github.com/google/uuid"
+
+ "cdr.dev/slog/v3"
+ "github.com/coder/coder/v2/coderd/database"
+)
+
// ConvertCallResultForTest exposes convertCallResult for external
// tests.
var ConvertCallResultForTest = convertCallResult
+// ConnectAllForTest exposes connectAll with an injectable connect
+// timeout and a reaperDone hook that fires after an abandoned
+// connect goroutine has been drained and its late session closed.
+func ConnectAllForTest(
+ ctx context.Context,
+ logger slog.Logger,
+ configs []database.MCPServerConfig,
+ timeout time.Duration,
+ reaperDone func(),
+) ([]fantasy.AgentTool, func()) {
+ return connectAllWithHooks(
+ ctx, logger, configs, nil, uuid.Nil, nil, nil,
+ timeout, connectHooks{reaperDone: reaperDone},
+ )
+}
+
// BuildAuthHeadersForTest exposes buildAuthHeaders for external
// tests.
var BuildAuthHeadersForTest = buildAuthHeaders
diff --git a/coderd/x/chatd/mcpclient/mcpclient.go b/coderd/x/chatd/mcpclient/mcpclient.go
index 4f2ffe9b638..e54e5a2d5ff 100644
--- a/coderd/x/chatd/mcpclient/mcpclient.go
+++ b/coderd/x/chatd/mcpclient/mcpclient.go
@@ -84,6 +84,32 @@ func ConnectAll(
userID uuid.UUID,
oidcSrc UserOIDCTokenSource,
coderHeaders map[string]string,
+) ([]fantasy.AgentTool, func()) {
+ return connectAllWithHooks(
+ ctx, logger, configs, tokens, userID, oidcSrc, coderHeaders,
+ connectTimeout, connectHooks{},
+ )
+}
+
+// connectHooks carries test-only instrumentation for connect
+// internals. The zero value is used in production.
+type connectHooks struct {
+ // reaperDone, when non-nil, is called after an abandoned
+ // connect goroutine's late result has been drained and any
+ // late session closed.
+ reaperDone func()
+}
+
+func connectAllWithHooks(
+ ctx context.Context,
+ logger slog.Logger,
+ configs []database.MCPServerConfig,
+ tokens []database.MCPServerUserToken,
+ userID uuid.UUID,
+ oidcSrc UserOIDCTokenSource,
+ coderHeaders map[string]string,
+ timeout time.Duration,
+ hooks connectHooks,
) ([]fantasy.AgentTool, func()) {
// Index tokens by server config ID so auth header
// construction is O(1) per server.
@@ -101,14 +127,20 @@ func ConnectAll(
)
// Build cleanup eagerly so it always closes any sessions
- // that connected, even if a later connection fails.
+ // that connected, even if a later connection fails. Each
+ // close runs in a detached goroutine: the sessions are
+ // discarded either way, and Close on a server that stopped
+ // responding mid-turn can block until the transport abandons
+ // the connection (the SDK detaches the request context), which
+ // must not stall the generation loop at step boundaries.
cleanup := func() {
mu.Lock()
- defer mu.Unlock()
- for _, s := range sessions {
- _ = s.Close()
- }
+ toClose := sessions
sessions = nil
+ mu.Unlock()
+ for _, s := range toClose {
+ go func() { _ = s.Close() }()
+ }
}
var eg errgroup.Group
@@ -120,6 +152,7 @@ func ConnectAll(
eg.Go(func() error {
serverTools, session, connectErr := connectOne(
ctx, logger, cfg, tokensByConfigID, userID, oidcSrc, coderHeaders,
+ timeout, hooks,
)
if connectErr != nil {
logger.Warn(ctx,
@@ -211,6 +244,8 @@ func connectOne(
userID uuid.UUID,
oidcSrc UserOIDCTokenSource,
coderHeaders map[string]string,
+ timeout time.Duration,
+ hooks connectHooks,
) ([]fantasy.AgentTool, *mcp.ClientSession, error) {
headers := buildAuthHeaders(ctx, logger, cfg, tokensByConfigID, userID, oidcSrc)
@@ -250,21 +285,64 @@ func connectOne(
// The timeout covers the entire connect+list sequence, not
// each phase individually. The SDK negotiates the protocol
// version during Connect; the session outlives connectCtx.
- connectCtx, cancel := context.WithTimeout(
- ctx, connectTimeout,
- )
+ connectCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
- session, err := mcpClient.Connect(connectCtx, tr, nil)
- if err != nil {
- return nil, nil, xerrors.Errorf("connect: %w", err)
+ // Run the connect+list sequence in a goroutine and enforce the
+ // budget externally. The SDK's streamable transport detaches
+ // the context after starting HTTP requests, and its error-path
+ // session.Close blocks on those detached requests, so a
+ // black-holed server can block Connect far past connectCtx's
+ // deadline. The select below guarantees the caller gets an
+ // answer within the budget regardless.
+ type connectResult struct {
+ session *mcp.ClientSession
+ tools *mcp.ListToolsResult
+ err error
+ }
+ resCh := make(chan connectResult, 1)
+ go func() {
+ session, err := mcpClient.Connect(connectCtx, tr, nil)
+ if err != nil {
+ resCh <- connectResult{err: xerrors.Errorf("connect: %w", err)}
+ return
+ }
+ toolsResult, err := session.ListTools(connectCtx, nil)
+ if err != nil {
+ // Deliver the result before closing: Close sends a
+ // DELETE on the SDK's detached context and can wedge,
+ // which would otherwise convert a fast ListTools
+ // failure into a budget timeout for the caller.
+ resCh <- connectResult{err: xerrors.Errorf("list tools: %w", err)}
+ _ = session.Close()
+ return
+ }
+ resCh <- connectResult{session: session, tools: toolsResult}
+ }()
+
+ var res connectResult
+ select {
+ case res = <-resCh:
+ case <-connectCtx.Done():
+ // Abandon the wedged goroutine; it exits once the
+ // transport's dial or response-header timeout fires. The
+ // reaper drains its late result and closes any session
+ // that still materialized so nothing leaks. It must not
+ // hold locks or block the caller.
+ go func() {
+ if late := <-resCh; late.session != nil {
+ _ = late.session.Close()
+ }
+ if hooks.reaperDone != nil {
+ hooks.reaperDone()
+ }
+ }()
+ return nil, nil, xerrors.Errorf("connect: %w", connectCtx.Err())
}
-
- toolsResult, err := session.ListTools(connectCtx, nil)
- if err != nil {
- _ = session.Close()
- return nil, nil, xerrors.Errorf("list tools: %w", err)
+ if res.err != nil {
+ return nil, nil, res.err
}
+ session, toolsResult := res.session, res.tools
var tools []fantasy.AgentTool
for _, mcpTool := range toolsResult.Tools {
@@ -286,7 +364,11 @@ func connectOne(
}
if len(tools) == 0 {
- _ = session.Close()
+ // Close the discarded session asynchronously: Close sends
+ // a DELETE on the SDK's detached context, so a server that
+ // wedges after a successful connect would otherwise hold
+ // the caller far past the connect budget.
+ go func() { _ = session.Close() }()
return nil, nil, nil
}
diff --git a/coderd/x/chatd/mcpclient/mcpclient_connect_test.go b/coderd/x/chatd/mcpclient/mcpclient_connect_test.go
new file mode 100644
index 00000000000..2152786bcad
--- /dev/null
+++ b/coderd/x/chatd/mcpclient/mcpclient_connect_test.go
@@ -0,0 +1,292 @@
+package mcpclient_test
+
+import (
+ "context"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/modelcontextprotocol/go-sdk/mcp"
+ "github.com/stretchr/testify/require"
+
+ "cdr.dev/slog/v3/sloggers/slogtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/x/chatd/mcpclient"
+)
+
+// blackHoleListener accepts TCP connections and never responds,
+// simulating a server (or an edge in front of it) that silently
+// drops requests. Returned connections are tracked so the test can
+// terminate them.
+type blackHoleListener struct {
+ ln net.Listener
+
+ mu sync.Mutex
+ conns []net.Conn
+}
+
+func newBlackHoleListener(t *testing.T) *blackHoleListener {
+ t.Helper()
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+ bh := &blackHoleListener{ln: ln}
+ go func() {
+ for {
+ conn, err := ln.Accept()
+ if err != nil {
+ return
+ }
+ bh.mu.Lock()
+ bh.conns = append(bh.conns, conn)
+ bh.mu.Unlock()
+ }
+ }()
+ t.Cleanup(bh.close)
+ return bh
+}
+
+func (b *blackHoleListener) close() {
+ _ = b.ln.Close()
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ for _, c := range b.conns {
+ _ = c.Close()
+ }
+ b.conns = nil
+}
+
+func (b *blackHoleListener) url() string {
+ return "http://" + b.ln.Addr().String()
+}
+
+// TestConnectAll_BlackHoledServerBudget is the acceptance test for
+// the connect budget: one black-holed server must not delay turn
+// preparation beyond the budget, and healthy servers' tools must
+// still be discovered. Without external budget enforcement the SDK
+// blocks several times past the context deadline (observed: 12s
+// for a 2s deadline) because its transport detaches the context.
+func TestConnectAll_BlackHoledServerBudget(t *testing.T) {
+ t.Parallel()
+ ctx := context.Background()
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
+
+ bh := newBlackHoleListener(t)
+ healthy := newTestMCPServer(t, echoTool())
+
+ reaperDone := make(chan struct{}, 2)
+ timeout := 1 * time.Second
+
+ start := time.Now()
+ tools, cleanup := mcpclient.ConnectAllForTest(ctx, logger,
+ []database.MCPServerConfig{
+ makeConfig("blackhole", bh.url()),
+ makeConfig("healthy", healthy.URL),
+ },
+ timeout,
+ func() { reaperDone <- struct{}{} },
+ )
+ elapsed := time.Since(start)
+ t.Cleanup(cleanup)
+
+ // The budget must hold: well under the SDK's unbounded
+ // behavior (6x the deadline), with margin for slow CI.
+ require.Less(t, elapsed, 4*timeout,
+ "ConnectAll took %s, budget was %s", elapsed, timeout)
+ require.Equal(t, []string{"healthy__echo"}, toolNames(tools))
+
+ // Terminating the black-holed connections unblocks the
+ // abandoned connect goroutine; the reaper must then drain its
+ // result and exit.
+ bh.close()
+ select {
+ case <-reaperDone:
+ case <-time.After(30 * time.Second):
+ t.Fatal("reaper did not exit after black-holed connections were closed")
+ }
+}
+
+// TestConnectAll_SlowServerStillConnects proves that a server that
+// is slow but within the budget still connects and serves tools.
+func TestConnectAll_SlowServerStillConnects(t *testing.T) {
+ t.Parallel()
+ ctx := context.Background()
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
+
+ srv := mcp.NewServer(&mcp.Implementation{Name: "slow", Version: "1.0.0"}, nil)
+ tool := echoTool()
+ srv.AddTool(tool.tool, tool.handler)
+ handler := mcp.NewStreamableHTTPHandler(
+ func(*http.Request) *mcp.Server { return srv },
+ &mcp.StreamableHTTPOptions{Stateless: true},
+ )
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(300 * time.Millisecond)
+ handler.ServeHTTP(w, r)
+ }))
+ t.Cleanup(ts.Close)
+
+ cfg := makeConfig("slow", ts.URL)
+ tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
+ t.Cleanup(cleanup)
+
+ require.Equal(t, []string{"slow__echo"}, toolNames(tools))
+}
+
+// TestConnectAll_LateServerReaped proves that when a server only
+// responds after the budget expired, ConnectAll has long returned
+// and the abandoned connect goroutine's late result is drained by
+// the reaper so nothing leaks.
+func TestConnectAll_LateServerReaped(t *testing.T) {
+ t.Parallel()
+ ctx := context.Background()
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
+
+ srv := mcp.NewServer(&mcp.Implementation{Name: "late", Version: "1.0.0"}, nil)
+ tool := echoTool()
+ srv.AddTool(tool.tool, tool.handler)
+ handler := mcp.NewStreamableHTTPHandler(
+ func(*http.Request) *mcp.Server { return srv },
+ &mcp.StreamableHTTPOptions{Stateless: true},
+ )
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(2 * time.Second)
+ handler.ServeHTTP(w, r)
+ }))
+ t.Cleanup(ts.Close)
+
+ reaperDone := make(chan struct{}, 1)
+ timeout := 500 * time.Millisecond
+
+ start := time.Now()
+ tools, cleanup := mcpclient.ConnectAllForTest(ctx, logger,
+ []database.MCPServerConfig{makeConfig("late", ts.URL)},
+ timeout,
+ func() { reaperDone <- struct{}{} },
+ )
+ elapsed := time.Since(start)
+ t.Cleanup(cleanup)
+
+ require.Less(t, elapsed, 4*timeout,
+ "ConnectAll took %s, budget was %s", elapsed, timeout)
+ require.Empty(t, tools)
+
+ select {
+ case <-reaperDone:
+ case <-time.After(30 * time.Second):
+ t.Fatal("reaper did not exit after the late server responded")
+ }
+}
+
+// TestConnectAll_CleanupPromptWhenServerWedges proves that the
+// cleanup function returns promptly even when a connected session's
+// server has stopped responding, so a wedged server cannot stall
+// the generation loop at step boundaries. The session teardown
+// DELETE is held server-side while cleanup must already have
+// returned.
+func TestConnectAll_CleanupPromptWhenServerWedges(t *testing.T) {
+ t.Parallel()
+ ctx := context.Background()
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
+
+ srv := mcp.NewServer(&mcp.Implementation{Name: "wedge", Version: "1.0.0"}, nil)
+ tool := echoTool()
+ srv.AddTool(tool.tool, tool.handler)
+ // Stateful handler so closing the session sends a DELETE.
+ handler := mcp.NewStreamableHTTPHandler(
+ func(*http.Request) *mcp.Server { return srv }, nil,
+ )
+
+ var deleteArrived atomic.Bool
+ releaseDelete := make(chan struct{})
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodDelete {
+ deleteArrived.Store(true)
+ select {
+ case <-releaseDelete:
+ case <-r.Context().Done():
+ }
+ }
+ handler.ServeHTTP(w, r)
+ }))
+ t.Cleanup(ts.Close)
+
+ var releaseOnce sync.Once
+ release := func() { releaseOnce.Do(func() { close(releaseDelete) }) }
+ // Registered after ts.Close so it runs before it, letting the
+ // wedged DELETE finish and the session unwind before the
+ // server waits for outstanding requests.
+ t.Cleanup(release)
+
+ cfg := makeConfig("wedge", ts.URL)
+ tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
+ require.Equal(t, []string{"wedge__echo"}, toolNames(tools))
+
+ start := time.Now()
+ cleanup()
+ elapsed := time.Since(start)
+ require.Less(t, elapsed, 1*time.Second,
+ "cleanup took %s with a wedged server", elapsed)
+
+ // The teardown must still happen in the background: the wedged
+ // DELETE arrives even though cleanup already returned.
+ require.Eventually(t, deleteArrived.Load,
+ 10*time.Second, 10*time.Millisecond,
+ "session close DELETE never reached the server")
+ release()
+}
+
+// TestConnectAll_NoToolsWedgedCloseWithinBudget proves that a
+// server whose session yields no usable tools cannot stall
+// ConnectAll past the connect budget when its teardown DELETE
+// wedges. The discarded session must be closed off the caller's
+// path, and the teardown must still happen in the background.
+func TestConnectAll_NoToolsWedgedCloseWithinBudget(t *testing.T) {
+ t.Parallel()
+ ctx := context.Background()
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
+
+ // Stateful handler with no registered tools so closing the
+ // discarded session sends a DELETE.
+ srv := mcp.NewServer(&mcp.Implementation{Name: "notools", Version: "1.0.0"}, nil)
+ handler := mcp.NewStreamableHTTPHandler(
+ func(*http.Request) *mcp.Server { return srv }, nil,
+ )
+
+ var deleteArrived atomic.Bool
+ releaseDelete := make(chan struct{})
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodDelete {
+ deleteArrived.Store(true)
+ select {
+ case <-releaseDelete:
+ case <-r.Context().Done():
+ }
+ }
+ handler.ServeHTTP(w, r)
+ }))
+ t.Cleanup(ts.Close)
+
+ var releaseOnce sync.Once
+ release := func() { releaseOnce.Do(func() { close(releaseDelete) }) }
+ t.Cleanup(release)
+
+ cfg := makeConfig("notools", ts.URL)
+ start := time.Now()
+ tools, cleanup := mcpclient.ConnectAll(ctx, logger, []database.MCPServerConfig{cfg}, nil, uuid.Nil, nil, nil)
+ elapsed := time.Since(start)
+ t.Cleanup(cleanup)
+
+ require.Empty(t, tools)
+ require.Less(t, elapsed, 5*time.Second,
+ "ConnectAll took %s with a wedged no-tools teardown", elapsed)
+
+ require.Eventually(t, deleteArrived.Load,
+ 10*time.Second, 10*time.Millisecond,
+ "discarded session close DELETE never reached the server")
+ release()
+}
diff --git a/coderd/x/chatd/mcpclient/mcphttpclient.go b/coderd/x/chatd/mcpclient/mcphttpclient.go
index d30f248dc79..e149aec86da 100644
--- a/coderd/x/chatd/mcpclient/mcphttpclient.go
+++ b/coderd/x/chatd/mcpclient/mcphttpclient.go
@@ -2,11 +2,55 @@ package mcpclient
import (
"flag"
+ "net"
"net/http"
+ "time"
)
+// dialTimeout bounds TCP connection establishment to an MCP
+// server. Without it, a server that silently drops SYNs (for
+// example an edge mitigation black-holing this deployment's
+// egress IPs) blocks the dial until the kernel gives up
+// retransmitting, roughly two minutes on Linux.
+const dialTimeout = 5 * time.Second
+
+// responseHeaderTimeout bounds how long a server may take to send
+// response headers once a request is written. It matches
+// toolCallTimeout rather than connectTimeout because the same
+// client serves tool-call POSTs, and a JSON-response MCP server
+// sends no headers until the tool finishes, so a lower value would
+// kill legitimate slow tools that fit the tool-call budget.
+// Long-lived SSE streams are unaffected; only their headers must
+// arrive within this window. http.Client.Timeout is deliberately
+// unset because it would cap the stream body too.
+const responseHeaderTimeout = toolCallTimeout
+
+// mcpSharedTransport is the transport for all production MCP
+// connections. MCP traffic must not use http.DefaultTransport
+// directly: the default has no dial or response-header bounds, so
+// a black-holed server would hold connections for minutes.
+var mcpSharedTransport = newMCPTransport()
+
+// newMCPTransport clones http.DefaultTransport when possible,
+// preserving proxy and connection-pool settings, and tightens its
+// failure timeouts so an unresponsive MCP server fails in seconds.
+func newMCPTransport() *http.Transport {
+ tr, ok := http.DefaultTransport.(*http.Transport)
+ if ok {
+ tr = tr.Clone()
+ } else {
+ tr = &http.Transport{Proxy: http.ProxyFromEnvironment}
+ }
+ tr.DialContext = (&net.Dialer{
+ Timeout: dialTimeout,
+ KeepAlive: 30 * time.Second,
+ }).DialContext
+ tr.ResponseHeaderTimeout = responseHeaderTimeout
+ return tr
+}
+
func httpClientWithHeaders(headers map[string]string) *http.Client {
- base := http.DefaultTransport
+ var base http.RoundTripper = mcpSharedTransport
if isolated := mcpHTTPClient(); isolated != nil {
base = isolated.Transport
}
@@ -33,20 +77,13 @@ func (h *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
}
// mcpHTTPClient returns an isolated *http.Client when running
-// inside tests, or nil for production. During tests,
-// httptest.Server.Close() calls
-// http.DefaultTransport.CloseIdleConnections(), which disrupts
-// any MCP client sharing that transport. When DefaultTransport
-// is a *http.Transport it is cloned; otherwise a minimal
-// transport with ProxyFromEnvironment is created as a fallback.
+// inside tests, or nil for production. During tests each client
+// gets a fresh transport so closed httptest servers cannot leave
+// stale pooled connections behind for later tests that reuse the
+// same address.
func mcpHTTPClient() *http.Client {
if flag.Lookup("test.v") == nil {
return nil
}
- if dt, ok := http.DefaultTransport.(*http.Transport); ok {
- return &http.Client{Transport: dt.Clone()}
- }
- return &http.Client{Transport: &http.Transport{
- Proxy: http.ProxyFromEnvironment,
- }}
+ return &http.Client{Transport: newMCPTransport()}
}
diff --git a/coderd/x/chatd/mcpclient/mcphttpclient_internal_test.go b/coderd/x/chatd/mcpclient/mcphttpclient_internal_test.go
new file mode 100644
index 00000000000..9c577511114
--- /dev/null
+++ b/coderd/x/chatd/mcpclient/mcphttpclient_internal_test.go
@@ -0,0 +1,27 @@
+package mcpclient
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+// TestMCPTransportTimeouts guards the transport hardening: MCP
+// traffic must never ride a transport without dial and
+// response-header bounds, or a black-holed server holds
+// connections until the kernel gives up (about two minutes).
+func TestMCPTransportTimeouts(t *testing.T) {
+ t.Parallel()
+
+ shared := mcpSharedTransport
+ require.NotNil(t, shared.DialContext)
+ require.Equal(t, responseHeaderTimeout, shared.ResponseHeaderTimeout)
+ // The response-header bound must not undercut the tool-call
+ // budget, or slow JSON-response tools within budget would be
+ // killed at the HTTP layer.
+ require.GreaterOrEqual(t, responseHeaderTimeout, toolCallTimeout)
+
+ isolated := mcpHTTPClient()
+ require.NotNil(t, isolated, "must be isolated under test")
+ require.NotSame(t, shared, isolated.Transport)
+}
From 87618a26ecc3fb2fceb6462f92f10834440cdea1 Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Tue, 25 Aug 2026 19:56:30 +0200
Subject: [PATCH 06/10] fix(coderd/x/chatd): truncate overlong generated chat
titles instead of rejecting them (#28589)
Chat title generation asks the model for a title in 2-8 words and then
hard-rejects any response longer than 8 words
(`validateGeneratedTitle`). Quickgen pins temperature for repeatable
output, so a model that overshoots the budget for a given conversation
overshoots on every retry: the rename dialog's Generate button returns a
deterministic 500 (`generate manual title: generated title exceeded 8
words`) no matter how often it is clicked, and the automatic
first-message path silently leaves the chat on its fallback title. On
dev.coder.com this rejection fires 1-3 times a day; one chat took 13
consecutive manual failures on 2026-08-25 (14:22-14:27 UTC).
Truncate the normalized title to the 8-word budget in
`normalizeTitleOutput` instead of rejecting it, and keep the empty-title
validation. Both the automatic and manual title paths share this
normalization, so both are fixed.
> Xum (AI agent) authored this change and PR on Mike's behalf.
(cherry picked from commit 60b03138ae2bf2f1e652764c268772d169f65ad0)
---
coderd/x/chatd/quickgen.go | 12 ++++--
coderd/x/chatd/quickgen_internal_test.go | 51 ++++++++++++++++++++++++
2 files changed, 60 insertions(+), 3 deletions(-)
diff --git a/coderd/x/chatd/quickgen.go b/coderd/x/chatd/quickgen.go
index c93bc9b7259..aea63d849f2 100644
--- a/coderd/x/chatd/quickgen.go
+++ b/coderd/x/chatd/quickgen.go
@@ -551,9 +551,6 @@ func validateGeneratedTitle(title string) error {
if title == "" {
return xerrors.New("generated title was empty")
}
- if len(strings.Fields(title)) > 8 {
- return xerrors.New("generated title exceeded 8 words")
- }
return nil
}
@@ -652,11 +649,20 @@ func titlePasteText(
return pasteText, nil
}
+// titleMaxWords caps generated titles at the prompt's stated 2-8 word
+// budget. Quickgen pins temperature, so a model that overshoots the
+// budget for a given conversation keeps overshooting on retry; keeping
+// the first words of an otherwise good title beats failing generation.
+const titleMaxWords = 8
+
func normalizeTitleOutput(title string) string {
title = normalizeShortTextOutput(title)
if title == "" {
return ""
}
+ if words := strings.Fields(title); len(words) > titleMaxWords {
+ title = strings.Join(words[:titleMaxWords], " ")
+ }
return truncateRunes(title, 80)
}
diff --git a/coderd/x/chatd/quickgen_internal_test.go b/coderd/x/chatd/quickgen_internal_test.go
index fe67c2da4a4..a6a8123191b 100644
--- a/coderd/x/chatd/quickgen_internal_test.go
+++ b/coderd/x/chatd/quickgen_internal_test.go
@@ -861,6 +861,32 @@ func TestNormalizeTurnStatusLabel(t *testing.T) {
}
}
+func Test_normalizeTitleOutput(t *testing.T) {
+ t.Parallel()
+
+ cases := []struct {
+ name string
+ input string
+ want string
+ }{
+ {name: "empty after normalization", input: " \"\" ", want: ""},
+ {name: "collapses whitespace and strips quotes", input: "\"Fix pq duplicate\tkey\"", want: "Fix pq duplicate key"},
+ {name: "keeps titles within the word budget", input: "Review risky changes in acme/webapp PR #123", want: "Review risky changes in acme/webapp PR #123"},
+ {
+ name: "truncates overlong titles to the word budget",
+ input: "Re-capture Tasks-enabled evidence for the Coder pull request feature flag",
+ want: "Re-capture Tasks-enabled evidence for the Coder pull request",
+ },
+ {name: "truncates to 80 runes", input: strings.Repeat("a", 100), want: strings.Repeat("a", 80)},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ require.Equal(t, tc.want, normalizeTitleOutput(tc.input))
+ })
+ }
+}
+
func TestFallbackTurnStatusLabel(t *testing.T) {
t.Parallel()
@@ -946,6 +972,31 @@ func TestGenerateStructuredTitleWithUsage_DropsRejectedTemperature(t *testing.T)
"generation should retry without temperature after the model rejects it")
}
+func TestGenerateStructuredTitleWithUsage_TruncatesOverlongTitle(t *testing.T) {
+ t.Parallel()
+
+ model := &chattest.FakeModel{
+ GenerateObjectFn: func(_ context.Context, _ fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
+ return &fantasy.ObjectResponse{
+ Object: map[string]any{
+ "title": "Re-capture Tasks-enabled evidence for the Coder pull request feature flag",
+ },
+ }, nil
+ },
+ }
+
+ title, _, err := generateStructuredTitleWithUsage(
+ t.Context(),
+ model,
+ titleObjectCall(resolvedModelCall{}),
+ titleGenerationPrompt,
+ "re-capture UI evidence for a Coder pull request",
+ )
+ require.NoError(t, err)
+ require.Equal(t, "Re-capture Tasks-enabled evidence for the Coder pull request", title,
+ "an overlong generated title should be truncated to the word budget, not rejected")
+}
+
func newOpenAICompatStructuredOutputServer(
t *testing.T,
toolName string,
From 63c3565d0f2b53a975344359742b956965ea0980 Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Wed, 26 Aug 2026 16:17:06 +0200
Subject: [PATCH 07/10] 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.
(cherry picked from commit 351bb1403250c834d5d065a01252b3283b238d72)
---
coderd/apidoc/docs.go | 5318 ++++++++++-------
coderd/apidoc/swagger.json | 4700 +++++++++------
coderd/chat_routes.go | 274 +
coderd/chat_routes_internal_test.go | 36 +
coderd/chat_routes_test.go | 65 +
coderd/coderd.go | 226 +-
coderd/coderdtest/swagger_test.go | 32 +
coderd/coderdtest/swaggerparser.go | 11 +-
coderd/exp_chats.go | 438 +-
coderd/exp_chats_acl.go | 10 +-
coderd/exp_chats_model_acl.go | 12 +-
coderd/httpmw/cors.go | 1 +
coderd/httpmw/ratelimit.go | 29 +-
coderd/httpmw/ratelimit_test.go | 47 +
coderd/mcp.go | 49 +-
coderd/mcp_acl.go | 12 +-
coderd/x/chatd/ARCHITECTURE.md | 8 +-
codersdk/chats.go | 8 +-
docs/ai-coder/agents/models.md | 4 +-
.../agents/platform-controls/mcp-servers.md | 2 +-
.../agents/platform-controls/organizations.md | 4 +-
.../agents/tasks-to-chats-migration.md | 32 +-
docs/reference/api/chats.md | 1931 ++++--
docs/reference/api/schemas.md | 424 ++
.../apidocgen/markdown-template/code_sh.dot | 6 +-
.../apidocgen/markdown-template/operation.dot | 2 +-
site/src/api/typesGenerated.ts | 6 +-
27 files changed, 8918 insertions(+), 4769 deletions(-)
create mode 100644 coderd/chat_routes.go
create mode 100644 coderd/chat_routes_internal_test.go
create mode 100644 coderd/chat_routes_test.go
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 18247541e41..5bba83571f5 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -154,38 +154,124 @@ const docTemplate = `{
}
}
},
- "/api/experimental/chats": {
+ "/api/experimental/chats/{chat}/stream/desktop": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
+ "description": "Raw binary WebSocket stream of the chat workspace desktop.\nExperimental: this endpoint is subject to change.",
"produces": [
- "application/json"
+ "application/octet-stream"
],
"tags": [
"Chats"
],
- "summary": "List chats",
- "operationId": "list-chats",
+ "summary": "Connect to chat workspace desktop via WebSockets",
+ "operationId": "connect-to-chat-workspace-desktop-via-websockets",
"parameters": [
{
"type": "string",
- "description": "Search query. Supports ` + "`" + `title:\u003csubstring\u003e` + "`" + ` (case-insensitive, quote multi-word values), ` + "`" + `archived:bool` + "`" + `, ` + "`" + `has_unread:bool` + "`" + `, ` + "`" + `pr_status:\u003cdraft\\|open\\|merged\\|closed\u003e` + "`" + ` as repeated or comma-separated values, ` + "`" + `source:\u003ccreated_by_me\\|shared_with_me\u003e` + "`" + `, ` + "`" + `diff_url:\u003curl\u003e` + "`" + ` (quote values containing colons), ` + "`" + `pr:\u003cnumber\u003e` + "`" + ` (exact PR number match), ` + "`" + `repo:\u003cowner/repo\u003e` + "`" + ` (case-insensitive substring match against git remote origin or URL), ` + "`" + `pr_title:\u003ctext\u003e` + "`" + ` (case-insensitive PR title substring), ` + "`" + `search:\u003ctext\u003e` + "`" + ` (full-text search across chat titles, PR titles, PR numbers, and message bodies; message bodies match English word stems, e.g. ` + "`" + `refactor` + "`" + ` matches ` + "`" + `refactoring` + "`" + `, and ignore English stopwords; titles and PR titles match whole words case-insensitively without stemming; quote multi-word values; cannot be combined with title, pr_title, or pr; a value that tokenizes to no searchable words, e.g. punctuation only, returns an empty list). Bare terms are not supported; use ` + "`" + `title:\u003cvalue\u003e` + "`" + ` or ` + "`" + `search:\u003cvalue\u003e` + "`" + `.",
- "name": "q",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "101": {
+ "description": "Switching Protocols"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/experimental/mcp/servers/{mcpServer}/oauth2/callback": {
+ "get": {
+ "produces": [
+ "text/html"
+ ],
+ "tags": [
+ "MCP"
+ ],
+ "summary": "Handle MCP server OAuth2 callback",
+ "operationId": "handle-mcp-server-oauth2-callback",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpServer",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Authorization code issued by the provider. Required together with state on success.",
+ "name": "code",
"in": "query"
},
{
"type": "string",
- "description": "Filter by label as key:value. Repeat for multiple (AND logic).",
- "name": "label",
+ "description": "Opaque state issued by the connect endpoint. Required together with code on success.",
+ "name": "state",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Provider error code. Present instead of code when authorization fails.",
+ "name": "error",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Provider error description accompanying error.",
+ "name": "error_description",
"in": "query"
}
],
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/experimental/users/{user}/skills": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Users"
+ ],
+ "summary": "List user skills",
+ "operationId": "list-user-skills",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/codersdk.Chat"
+ "$ref": "#/definitions/codersdk.UserSkillMetadata"
}
}
}
@@ -194,10 +280,12 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
},
"post": {
- "description": "Experimental: this endpoint is subject to change.",
"consumes": [
"application/json"
],
@@ -205,18 +293,25 @@ const docTemplate = `{
"application/json"
],
"tags": [
- "Chats"
+ "Users"
],
- "summary": "Create chat",
- "operationId": "create-chat",
+ "summary": "Create a user skill",
+ "operationId": "create-a-user-skill",
"parameters": [
{
- "description": "Create chat request",
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Create user skill request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateChatRequest"
+ "$ref": "#/definitions/codersdk.CreateUserSkillRequest"
}
}
],
@@ -224,13 +319,7 @@ const docTemplate = `{
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.Chat"
- }
- },
- "413": {
- "description": "Request body exceeds 256 KiB",
- "schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "$ref": "#/definitions/codersdk.UserSkill"
}
}
},
@@ -238,24 +327,43 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/experimental/chats/config/retention-days": {
+ "/api/experimental/users/{user}/skills/{skillName}": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Chats"
+ "Users"
+ ],
+ "summary": "Get a user skill by name",
+ "operationId": "get-a-user-skill-by-name",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Skill name",
+ "name": "skillName",
+ "in": "path",
+ "required": true
+ }
],
- "summary": "Get chat retention days",
- "operationId": "get-chat-retention-days",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatRetentionDaysResponse"
+ "$ref": "#/definitions/codersdk.UserSkill"
}
}
},
@@ -268,24 +376,26 @@ const docTemplate = `{
"skip": true
}
},
- "put": {
- "consumes": [
- "application/json"
- ],
+ "delete": {
"tags": [
- "Chats"
+ "Users"
],
- "summary": "Update chat retention days",
- "operationId": "update-chat-retention-days",
+ "summary": "Delete a user skill",
+ "operationId": "delete-a-user-skill",
"parameters": [
{
- "description": "Request body",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.UpdateChatRetentionDaysRequest"
- }
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Skill name",
+ "name": "skillName",
+ "in": "path",
+ "required": true
}
],
"responses": {
@@ -301,51 +411,49 @@ const docTemplate = `{
"x-apidocgen": {
"skip": true
}
- }
- },
- "/api/experimental/chats/files": {
- "post": {
- "description": "Experimental: this endpoint is subject to change.",
+ },
+ "patch": {
"consumes": [
- "image/png",
- "image/jpeg",
- "image/gif",
- "image/webp",
- "text/plain",
- "text/markdown",
- "text/csv",
- "application/json",
- "application/pdf"
+ "application/json"
],
"produces": [
"application/json"
],
"tags": [
- "Chats"
+ "Users"
],
- "summary": "Upload chat file",
- "operationId": "upload-chat-file",
+ "summary": "Update a user skill",
+ "operationId": "update-a-user-skill",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "query",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Skill name",
+ "name": "skillName",
+ "in": "path",
"required": true
+ },
+ {
+ "description": "Update user skill request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateUserSkillRequest"
+ }
}
],
"responses": {
- "201": {
- "description": "Created",
- "schema": {
- "$ref": "#/definitions/codersdk.UploadChatFileResponse"
- }
- },
- "413": {
- "description": "Request body exceeds 10 MiB",
+ "200": {
+ "description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "$ref": "#/definitions/codersdk.UserSkill"
}
}
},
@@ -353,122 +461,135 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/experimental/chats/files/{file}": {
+ "/api/experimental/watch-all-workspacebuilds": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
"produces": [
- "image/png",
- "image/jpeg",
- "image/gif",
- "image/webp",
- "text/plain",
- "text/markdown",
- "text/csv",
- "application/json",
- "application/pdf"
+ "application/json"
],
"tags": [
- "Chats"
- ],
- "summary": "Get chat file",
- "operationId": "get-chat-file",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "File ID",
- "name": "file",
- "in": "path",
- "required": true
- }
+ "Workspaces"
],
+ "summary": "Watch all workspace builds",
+ "operationId": "watch-all-workspace-builds",
"responses": {
- "200": {
- "description": "OK"
+ "101": {
+ "description": "Switching Protocols"
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/experimental/chats/files/{file}/download": {
+ "/api/v2/": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
"produces": [
- "image/png",
- "image/jpeg",
- "image/gif",
- "image/webp",
- "text/plain",
- "text/markdown",
- "text/csv",
- "application/json",
- "application/pdf"
+ "application/json"
],
"tags": [
- "Chats"
+ "General"
],
- "summary": "Download chat file with signed token",
- "operationId": "download-chat-file",
+ "summary": "API root handler",
+ "operationId": "api-root-handler",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
+ }
+ }
+ }
+ }
+ },
+ "/api/v2/agent-firewall/sessions/{id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Enterprise"
+ ],
+ "summary": "Get agent firewall session by ID",
+ "operationId": "get-agent-firewall-session-by-id",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "File ID",
- "name": "file",
+ "description": "Agent firewall session ID",
+ "name": "id",
"in": "path",
"required": true
- },
- {
- "type": "string",
- "description": "Signed download token",
- "name": "token",
- "in": "query",
- "required": true
}
],
"responses": {
"200": {
- "description": "OK"
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.AgentFirewallSession"
+ }
}
},
- "x-apidocgen": {
- "skip": true
- }
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
}
},
- "/api/experimental/chats/files/{file}/download-url": {
- "post": {
- "description": "Experimental: this endpoint is subject to change.",
+ "/api/v2/agent-firewall/sessions/{id}/logs": {
+ "get": {
"produces": [
"application/json"
],
"tags": [
- "Chats"
+ "Enterprise"
],
- "summary": "Create chat file download URL",
- "operationId": "create-chat-file-download-url",
+ "summary": "Get agent firewall session logs",
+ "operationId": "get-agent-firewall-session-logs",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "File ID",
- "name": "file",
+ "description": "Agent firewall session ID",
+ "name": "id",
"in": "path",
"required": true
+ },
+ {
+ "type": "integer",
+ "description": "Inclusive lower bound on sequence number",
+ "name": "seq_after",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Exclusive upper bound on sequence number",
+ "name": "seq_before",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Maximum number of logs to return (default 100)",
+ "name": "limit",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatFileDownloadURLResponse"
+ "$ref": "#/definitions/codersdk.AgentFirewallSessionLogsResponse"
}
}
},
@@ -476,28 +597,28 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/chats/watch": {
+ "/api/v2/ai-gateway/clients": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
+ "description": "Alias: also available at /api/v2/aibridge/clients for backward compatibility.",
"produces": [
"application/json"
],
"tags": [
- "Chats"
+ "AI Gateway"
],
- "summary": "Watch chat events for a user via WebSockets",
- "operationId": "watch-chat-events-for-a-user-via-websockets",
+ "summary": "List AI Gateway clients",
+ "operationId": "list-ai-gateway-clients",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatWatchEvent"
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
}
},
@@ -508,32 +629,24 @@ const docTemplate = `{
]
}
},
- "/api/experimental/chats/{chat}": {
+ "/api/v2/ai-gateway/keys": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
"produces": [
"application/json"
],
"tags": [
- "Chats"
- ],
- "summary": "Get chat by ID",
- "operationId": "get-chat-by-id",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- }
+ "Enterprise"
],
+ "summary": "List AI Gateway keys",
+ "operationId": "list-ai-gateway-keys",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Chat"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.AIGatewayKey"
+ }
}
}
},
@@ -543,38 +656,35 @@ const docTemplate = `{
}
]
},
- "patch": {
- "description": "Experimental: this endpoint is subject to change.",
+ "post": {
"consumes": [
"application/json"
],
+ "produces": [
+ "application/json"
+ ],
"tags": [
- "Chats"
+ "Enterprise"
],
- "summary": "Update chat",
- "operationId": "update-chat",
+ "summary": "Create AI Gateway key",
+ "operationId": "create-ai-gateway-key",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- },
- {
- "description": "Update chat request",
+ "description": "Create AI Gateway key request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateChatRequest"
+ "$ref": "#/definitions/codersdk.CreateAIGatewayKeyRequest"
}
}
],
"responses": {
- "204": {
- "description": "No Content"
+ "201": {
+ "description": "Created",
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateAIGatewayKeyResponse"
+ }
}
},
"security": [
@@ -584,114 +694,125 @@ const docTemplate = `{
]
}
},
- "/api/experimental/chats/{chat}/acl": {
- "get": {
- "description": "Experimental: this endpoint is subject to change.",
- "produces": [
- "application/json"
- ],
+ "/api/v2/ai-gateway/keys/{key}": {
+ "delete": {
"tags": [
- "Chats"
+ "Enterprise"
],
- "summary": "Get chat ACLs",
- "operationId": "get-chat-acls",
+ "summary": "Delete AI Gateway key",
+ "operationId": "delete-ai-gateway-key",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "Chat ID",
- "name": "chat",
+ "description": "Key ID",
+ "name": "key",
"in": "path",
"required": true
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.ChatACL"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
- },
- "patch": {
- "description": "Experimental: this endpoint is subject to change.",
- "consumes": [
+ ]
+ }
+ },
+ "/api/v2/ai-gateway/models": {
+ "get": {
+ "description": "Alias: also available at /api/v2/aibridge/models for backward compatibility.",
+ "produces": [
"application/json"
],
"tags": [
- "Chats"
+ "AI Gateway"
],
- "summary": "Update chat ACL",
- "operationId": "update-chat-acl",
- "parameters": [
+ "summary": "List AI Gateway models",
+ "operationId": "list-ai-gateway-models",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "security": [
{
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- },
- {
- "description": "Update chat ACL request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.UpdateChatACL"
- }
+ "CoderSessionToken": []
}
+ ]
+ }
+ },
+ "/api/v2/ai-gateway/serve": {
+ "get": {
+ "tags": [
+ "Enterprise"
],
+ "summary": "AI Gateway serve",
+ "operationId": "ai-gateway-serve",
"responses": {
- "204": {
- "description": "No Content"
+ "101": {
+ "description": "Switching Protocols"
}
},
"security": [
{
- "CoderSessionToken": []
+ "AIGatewayKey": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/chats/{chat}/compact": {
- "post": {
- "description": "Experimental: this endpoint is subject to change.\nRequests a manual context compaction on an idle or errored\nchat, clearing any stored error. The compaction runs\nasynchronously through the chat worker and bypasses the\nautomatic usage threshold.",
+ "/api/v2/ai-gateway/sessions": {
+ "get": {
+ "description": "Alias: also available at /api/v2/aibridge/sessions for backward compatibility.",
"produces": [
"application/json"
],
"tags": [
- "Chats"
+ "AI Gateway"
],
- "summary": "Compact chat",
- "operationId": "compact-chat",
+ "summary": "List AI Gateway sessions",
+ "operationId": "list-ai-gateway-sessions",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
+ "description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before.",
+ "name": "q",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page limit",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Cursor pagination after session ID (cannot be used with offset)",
+ "name": "after_session_id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Offset pagination (cannot be used with after_session_id)",
+ "name": "offset",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Chat"
+ "$ref": "#/definitions/codersdk.AIBridgeListSessionsResponse"
}
}
},
@@ -699,38 +820,52 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/chats/{chat}/context": {
- "put": {
- "description": "Experimental: this endpoint is subject to change.",
+ "/api/v2/ai-gateway/sessions/{session_id}": {
+ "get": {
+ "description": "Alias: also available at /api/v2/aibridge/sessions/{session_id} for backward compatibility.",
"produces": [
"application/json"
],
"tags": [
- "Chats"
+ "AI Gateway"
],
- "summary": "Refresh chat context",
- "operationId": "refresh-chat-context",
+ "summary": "Get AI Gateway session threads",
+ "operationId": "get-ai-gateway-session-threads",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
+ "description": "Session ID (client_session_id or interception UUID)",
+ "name": "session_id",
"in": "path",
"required": true
+ },
+ {
+ "type": "string",
+ "description": "Thread pagination cursor (forward/older)",
+ "name": "after_id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Thread pagination cursor (backward/newer)",
+ "name": "before_id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Number of threads per page (default 50)",
+ "name": "limit",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Chat"
+ "$ref": "#/definitions/codersdk.AIBridgeSessionThreadsResponse"
}
}
},
@@ -741,32 +876,24 @@ const docTemplate = `{
]
}
},
- "/api/experimental/chats/{chat}/cost": {
+ "/api/v2/ai/providers": {
"get": {
- "description": "Experimental: this endpoint is subject to change.\n\nCost covers the whole chat tree: the root chat plus every\nsubagent chat beneath it. Requesting cost for a subagent chat\nreturns that same total.\n\nCost is derived from AI Gateway data, which is subject to its\nown retention period, 60 days by default, configured\nindependently of chat retention. Spend for requests older than\nthat period is no longer reported, so a chat whose requests\nhave all been purged reports zero cost.",
"produces": [
"application/json"
],
"tags": [
- "Chats"
- ],
- "summary": "Get chat cost",
- "operationId": "get-chat-cost",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- }
+ "AI Providers"
],
+ "summary": "List AI providers",
+ "operationId": "list-ai-providers",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatCost"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.AIProvider"
+ }
}
}
},
@@ -775,34 +902,35 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- }
- },
- "/api/experimental/chats/{chat}/diff": {
- "get": {
- "description": "Experimental: this endpoint is subject to change.",
+ },
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
"produces": [
"application/json"
],
"tags": [
- "Chats"
+ "AI Providers"
],
- "summary": "Get chat diff contents",
- "operationId": "get-chat-diff-contents",
+ "summary": "Create an AI provider",
+ "operationId": "create-an-ai-provider",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
+ "description": "Create AI provider request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateAIProviderRequest"
+ }
}
],
"responses": {
- "200": {
- "description": "OK",
+ "201": {
+ "description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.ChatDiffContents"
+ "$ref": "#/definitions/codersdk.AIProvider"
}
}
},
@@ -813,23 +941,21 @@ const docTemplate = `{
]
}
},
- "/api/experimental/chats/{chat}/interrupt": {
- "post": {
- "description": "Experimental: this endpoint is subject to change.",
+ "/api/v2/ai/providers/{idOrName}": {
+ "get": {
"produces": [
"application/json"
],
"tags": [
- "Chats"
+ "AI Providers"
],
- "summary": "Interrupt chat",
- "operationId": "interrupt-chat",
+ "summary": "Get an AI provider",
+ "operationId": "get-an-ai-provider",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
+ "description": "Provider ID or name",
+ "name": "idOrName",
"in": "path",
"required": true
}
@@ -838,7 +964,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Chat"
+ "$ref": "#/definitions/codersdk.AIProvider"
}
}
},
@@ -847,53 +973,25 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- }
- },
- "/api/experimental/chats/{chat}/messages": {
- "get": {
- "description": "Experimental: this endpoint is subject to change.",
- "produces": [
- "application/json"
- ],
+ },
+ "delete": {
"tags": [
- "Chats"
+ "AI Providers"
],
- "summary": "List chat messages",
- "operationId": "list-chat-messages",
+ "summary": "Delete an AI provider",
+ "operationId": "delete-an-ai-provider",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
+ "description": "Provider ID or name",
+ "name": "idOrName",
"in": "path",
"required": true
- },
- {
- "type": "integer",
- "description": "Return messages with id \u003c before_id",
- "name": "before_id",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Return messages with id \u003e after_id",
- "name": "after_id",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Page size, 1 to 200. Defaults to 50.",
- "name": "limit",
- "in": "query"
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.ChatMessagesResponse"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
@@ -902,8 +1000,7 @@ const docTemplate = `{
}
]
},
- "post": {
- "description": "Experimental: this endpoint is subject to change.",
+ "patch": {
"consumes": [
"application/json"
],
@@ -911,26 +1008,25 @@ const docTemplate = `{
"application/json"
],
"tags": [
- "Chats"
+ "AI Providers"
],
- "summary": "Send chat message",
- "operationId": "send-chat-message",
+ "summary": "Update an AI provider",
+ "operationId": "update-an-ai-provider",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
+ "description": "Provider ID or name",
+ "name": "idOrName",
"in": "path",
"required": true
},
{
- "description": "Create chat message request",
+ "description": "Update AI provider request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateChatMessageRequest"
+ "$ref": "#/definitions/codersdk.UpdateAIProviderRequest"
}
}
],
@@ -938,7 +1034,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.CreateChatMessageResponse"
+ "$ref": "#/definitions/codersdk.AIProvider"
}
}
},
@@ -949,51 +1045,21 @@ const docTemplate = `{
]
}
},
- "/api/experimental/chats/{chat}/messages/{message}": {
- "patch": {
- "description": "Experimental: this endpoint is subject to change.",
- "consumes": [
- "application/json"
- ],
+ "/api/v2/appearance": {
+ "get": {
"produces": [
"application/json"
],
"tags": [
- "Chats"
- ],
- "summary": "Edit chat message",
- "operationId": "edit-chat-message",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- },
- {
- "type": "integer",
- "description": "Message ID",
- "name": "message",
- "in": "path",
- "required": true
- },
- {
- "description": "Edit chat message request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.EditChatMessageRequest"
- }
- }
+ "Enterprise"
],
+ "summary": "Get appearance",
+ "operationId": "get-appearance",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.EditChatMessageResponse"
+ "$ref": "#/definitions/codersdk.AppearanceConfig"
}
}
},
@@ -1002,40 +1068,35 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- }
- },
- "/api/experimental/chats/{chat}/prompts": {
- "get": {
- "description": "Experimental: this endpoint is subject to change.\n\nReturns the user-authored prompts in a chat, newest first,\nwith each prompt's text parts concatenated in the order they\nwere authored. Used by the composer to power the up/down\narrow prompt-history cycle without paging through every\nmessage in the chat.",
+ },
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
"produces": [
"application/json"
],
"tags": [
- "Chats"
+ "Enterprise"
],
- "summary": "List chat user prompts",
- "operationId": "list-chat-user-prompts",
+ "summary": "Update appearance",
+ "operationId": "update-appearance",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- },
- {
- "type": "integer",
- "description": "Page size, 0 to 2000. 0 (the default) means the server-side default of 500.",
- "name": "limit",
- "in": "query"
+ "description": "Update appearance request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateAppearanceConfig"
+ }
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatPromptsResponse"
+ "$ref": "#/definitions/codersdk.UpdateAppearanceConfig"
}
}
},
@@ -1046,33 +1107,24 @@ const docTemplate = `{
]
}
},
- "/api/experimental/chats/{chat}/reconcile-invalid": {
- "post": {
- "description": "Experimental: this endpoint is subject to change.",
- "produces": [
- "application/json"
- ],
+ "/api/v2/applications/auth-redirect": {
+ "get": {
"tags": [
- "Chats"
+ "Applications"
],
- "summary": "Reconcile invalid chat state",
- "operationId": "reconcile-invalid-chat-state",
+ "summary": "Redirect to URI with encrypted API key",
+ "operationId": "redirect-to-uri-with-encrypted-api-key",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
+ "description": "Redirect destination",
+ "name": "redirect_uri",
+ "in": "query"
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.Chat"
- }
+ "307": {
+ "description": "Temporary Redirect"
}
},
"security": [
@@ -1082,32 +1134,22 @@ const docTemplate = `{
]
}
},
- "/api/experimental/chats/{chat}/stream": {
+ "/api/v2/applications/host": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
"produces": [
"application/json"
],
"tags": [
- "Chats"
- ],
- "summary": "Stream chat events via WebSockets",
- "operationId": "stream-chat-events-via-websockets",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- }
+ "Applications"
],
+ "summary": "Get applications host",
+ "operationId": "get-applications-host",
+ "deprecated": true,
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatStreamEvent"
+ "$ref": "#/definitions/codersdk.AppHostResponse"
}
}
},
@@ -1118,65 +1160,84 @@ const docTemplate = `{
]
}
},
- "/api/experimental/chats/{chat}/stream/desktop": {
- "get": {
- "description": "Raw binary WebSocket stream of the chat workspace desktop.\nExperimental: this endpoint is subject to change.",
+ "/api/v2/applications/reconnecting-pty-signed-token": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
"produces": [
- "application/octet-stream"
+ "application/json"
],
"tags": [
- "Chats"
+ "Enterprise"
],
- "summary": "Connect to chat workspace desktop via WebSockets",
- "operationId": "connect-to-chat-workspace-desktop-via-websockets",
+ "summary": "Issue signed app token for reconnecting PTY",
+ "operationId": "issue-signed-app-token-for-reconnecting-pty",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
+ "description": "Issue reconnecting PTY signed token request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.IssueReconnectingPTYSignedTokenRequest"
+ }
}
],
"responses": {
- "101": {
- "description": "Switching Protocols"
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.IssueReconnectingPTYSignedTokenResponse"
+ }
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/experimental/chats/{chat}/stream/git": {
+ "/api/v2/audit": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
"produces": [
"application/json"
],
"tags": [
- "Chats"
+ "Audit"
],
- "summary": "Watch chat workspace git state via WebSockets",
- "operationId": "watch-chat-workspace-git-state-via-websockets",
+ "summary": "Get audit logs",
+ "operationId": "get-audit-logs",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
+ "description": "Search query",
+ "name": "q",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page limit",
+ "name": "limit",
+ "in": "query",
"required": true
+ },
+ {
+ "type": "integer",
+ "description": "Page offset",
+ "name": "offset",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.WorkspaceAgentGitServerMessage"
+ "$ref": "#/definitions/codersdk.AuditLogResponse"
}
}
},
@@ -1187,33 +1248,30 @@ const docTemplate = `{
]
}
},
- "/api/experimental/chats/{chat}/stream/parts": {
- "get": {
- "description": "Experimental: this endpoint is subject to change.",
- "produces": [
+ "/api/v2/audit/testgenerate": {
+ "post": {
+ "consumes": [
"application/json"
],
"tags": [
- "Chats"
+ "Audit"
],
- "summary": "Stream chat parts via WebSockets",
- "operationId": "stream-chat-parts-via-websockets",
+ "summary": "Generate fake audit log",
+ "operationId": "generate-fake-audit-log",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
+ "description": "Audit log request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateTestAuditLogRequest"
+ }
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.ChatStreamEvent"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
@@ -1226,32 +1284,55 @@ const docTemplate = `{
}
}
},
- "/api/experimental/chats/{chat}/title/propose": {
+ "/api/v2/auth/scopes": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authorization"
+ ],
+ "summary": "List API key scopes",
+ "operationId": "list-api-key-scopes",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ExternalAPIKeyScopes"
+ }
+ }
+ }
+ }
+ },
+ "/api/v2/authcheck": {
"post": {
- "description": "Experimental: this endpoint is subject to change.",
+ "consumes": [
+ "application/json"
+ ],
"produces": [
"application/json"
],
"tags": [
- "Chats"
+ "Authorization"
],
- "summary": "Propose chat title",
- "operationId": "propose-chat-title",
+ "summary": "Check authorization",
+ "operationId": "check-authorization",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
+ "description": "Authorization request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.AuthorizationRequest"
+ }
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ProposeChatTitleResponse"
+ "$ref": "#/definitions/codersdk.AuthorizationResponse"
}
}
},
@@ -1262,90 +1343,124 @@ const docTemplate = `{
]
}
},
- "/api/experimental/mcp/servers/{mcpServer}/oauth2/callback": {
+ "/api/v2/buildinfo": {
"get": {
"produces": [
- "text/html"
+ "application/json"
],
"tags": [
- "MCP"
+ "General"
],
- "summary": "Handle MCP server OAuth2 callback",
- "operationId": "handle-mcp-server-oauth2-callback",
+ "summary": "Build info",
+ "operationId": "build-info",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.BuildInfoResponse"
+ }
+ }
+ }
+ }
+ },
+ "/api/v2/chats": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Chats"
+ ],
+ "summary": "List chats",
+ "operationId": "list-chats",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpServer",
- "in": "path",
- "required": true
+ "description": "Search query. Supports ` + "`" + `title:\u003csubstring\u003e` + "`" + ` (case-insensitive, quote multi-word values), ` + "`" + `archived:bool` + "`" + `, ` + "`" + `has_unread:bool` + "`" + `, ` + "`" + `pr_status:\u003cdraft\\|open\\|merged\\|closed\u003e` + "`" + ` as repeated or comma-separated values, ` + "`" + `source:\u003ccreated_by_me\\|shared_with_me\u003e` + "`" + `, ` + "`" + `diff_url:\u003curl\u003e` + "`" + ` (quote values containing colons), ` + "`" + `pr:\u003cnumber\u003e` + "`" + ` (exact PR number match), ` + "`" + `repo:\u003cowner/repo\u003e` + "`" + ` (case-insensitive substring match against git remote origin or URL), ` + "`" + `pr_title:\u003ctext\u003e` + "`" + ` (case-insensitive PR title substring), ` + "`" + `search:\u003ctext\u003e` + "`" + ` (full-text search across chat titles, PR titles, PR numbers, and message bodies; message bodies match English word stems, e.g. ` + "`" + `refactor` + "`" + ` matches ` + "`" + `refactoring` + "`" + `, and ignore English stopwords; titles and PR titles match whole words case-insensitively without stemming; quote multi-word values; cannot be combined with title, pr_title, or pr; a value that tokenizes to no searchable words, e.g. punctuation only, returns an empty list). Bare terms are not supported; use ` + "`" + `title:\u003cvalue\u003e` + "`" + ` or ` + "`" + `search:\u003cvalue\u003e` + "`" + `.",
+ "name": "q",
+ "in": "query"
},
{
- "type": "string",
- "description": "Authorization code issued by the provider. Required together with state on success.",
- "name": "code",
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "collectionFormat": "multi",
+ "description": "Filter by label as key:value. Repeat for multiple (AND logic).",
+ "name": "label",
"in": "query"
},
{
"type": "string",
- "description": "Opaque state issued by the connect endpoint. Required together with code on success.",
- "name": "state",
+ "format": "uuid",
+ "description": "After ID",
+ "name": "after_id",
"in": "query"
},
{
- "type": "string",
- "description": "Provider error code. Present instead of code when authorization fails.",
- "name": "error",
+ "type": "integer",
+ "description": "Page limit",
+ "name": "limit",
"in": "query"
},
{
- "type": "string",
- "description": "Provider error description accompanying error.",
- "name": "error_description",
+ "type": "integer",
+ "description": "Page offset",
+ "name": "offset",
"in": "query"
}
],
"responses": {
"200": {
- "description": "OK"
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.Chat"
+ }
+ }
}
},
"security": [
{
"CoderSessionToken": []
}
+ ]
+ },
+ "post": {
+ "consumes": [
+ "application/json"
],
- "x-apidocgen": {
- "skip": true
- }
- }
- },
- "/api/experimental/mcp/servers/{mcpServer}/oauth2/disconnect": {
- "delete": {
"produces": [
"application/json"
],
"tags": [
- "MCP"
+ "Chats"
],
- "summary": "Disconnect MCP server OAuth2 token",
- "operationId": "disconnect-mcp-server-oauth2-token",
+ "summary": "Create chat",
+ "operationId": "create-chat",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpServer",
- "in": "path",
- "required": true
+ "description": "Create chat request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateChatRequest"
+ }
}
],
"responses": {
- "200": {
- "description": "OK",
+ "201": {
+ "description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.MCPServerOAuth2DisconnectResponse"
+ "$ref": "#/definitions/codersdk.Chat"
+ }
+ },
+ "413": {
+ "description": "Request body exceeds 256 KiB",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
}
}
},
@@ -1353,13 +1468,10 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/chats/model-overrides": {
+ "/api/v2/chats/by-workspace": {
"get": {
"produces": [
"application/json"
@@ -1367,22 +1479,21 @@ const docTemplate = `{
"tags": [
"Chats"
],
- "summary": "List organization chat model overrides",
- "operationId": "list-organization-chat-model-overrides",
+ "summary": "List chats by workspace",
+ "operationId": "list-chats-by-workspace",
"parameters": [
{
"type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
+ "description": "Comma-separated workspace IDs",
+ "name": "workspace_ids",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatModelOverridesResponse"
+ "$ref": "#/definitions/coderd.chatsByWorkspaceResponse"
}
}
},
@@ -1390,76 +1501,66 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/chats/model-overrides/{context}": {
- "put": {
- "consumes": [
+ "/api/v2/chats/config/auto-archive-days": {
+ "get": {
+ "produces": [
"application/json"
],
- "produces": [
+ "tags": [
+ "Chats"
+ ],
+ "summary": "Get chat auto archive days",
+ "operationId": "get-chat-auto-archive-days",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatAutoArchiveDaysResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "put": {
+ "consumes": [
"application/json"
],
"tags": [
"Chats"
],
- "summary": "Update organization chat model override",
- "operationId": "update-organization-chat-model-override",
+ "summary": "Update chat auto archive days",
+ "operationId": "update-chat-auto-archive-days",
"parameters": [
{
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "enum": [
- "general",
- "explore",
- "title_generation",
- "compaction",
- "advisor"
- ],
- "type": "string",
- "description": "Override context",
- "name": "context",
- "in": "path",
- "required": true
- },
- {
- "description": "Model override",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateChatModelOverrideRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatAutoArchiveDaysRequest"
}
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.ChatModelOverrideResponse"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/chats/models": {
+ "/api/v2/chats/config/debug-logging": {
"get": {
"produces": [
"application/json"
@@ -1467,22 +1568,13 @@ const docTemplate = `{
"tags": [
"Chats"
],
- "summary": "List AI models and provider descriptors in an organization",
- "operationId": "list-ai-models-by-organization",
- "parameters": [
- {
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- }
- ],
+ "summary": "Get chat debug logging setting",
+ "operationId": "get-chat-debug-logging-setting",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.OrganizationChatModelsResponse"
+ "$ref": "#/definitions/codersdk.ChatDebugLoggingAdminSettings"
}
}
},
@@ -1490,60 +1582,41 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
- "post": {
+ "put": {
"consumes": [
"application/json"
],
- "produces": [
- "application/json"
- ],
"tags": [
"Chats"
],
- "summary": "Create an AI model in an organization",
- "operationId": "create-ai-model",
+ "summary": "Update chat debug logging setting",
+ "operationId": "update-chat-debug-logging-setting",
"parameters": [
{
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "description": "Model",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateChatModelRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatDebugLoggingAllowUsersRequest"
}
}
],
"responses": {
- "201": {
- "description": "Created",
- "schema": {
- "$ref": "#/definitions/codersdk.ChatModel"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/chats/models/{model}": {
+ "/api/v2/chats/config/debug-retention-days": {
"get": {
"produces": [
"application/json"
@@ -1551,29 +1624,13 @@ const docTemplate = `{
"tags": [
"Chats"
],
- "summary": "Get an AI model",
- "operationId": "get-ai-model",
- "parameters": [
- {
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "Model ID",
- "name": "model",
- "in": "path",
- "required": true
- }
- ],
+ "summary": "Get chat debug retention days",
+ "operationId": "get-chat-debug-retention-days",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatModel"
+ "$ref": "#/definitions/codersdk.ChatDebugRetentionDaysResponse"
}
}
},
@@ -1581,31 +1638,26 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
- "delete": {
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
"tags": [
"Chats"
],
- "summary": "Delete an AI model",
- "operationId": "delete-ai-model",
+ "summary": "Update chat debug retention days",
+ "operationId": "update-chat-debug-retention-days",
"parameters": [
{
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "Model ID",
- "name": "model",
- "in": "path",
- "required": true
+ "description": "Request body",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateChatDebugRetentionDaysRequest"
+ }
}
],
"responses": {
@@ -1617,67 +1669,66 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
+ ]
+ }
+ },
+ "/api/v2/chats/config/personal-model-overrides": {
+ "get": {
+ "produces": [
+ "application/json"
],
- "x-apidocgen": {
- "skip": true
- }
+ "tags": [
+ "Chats"
+ ],
+ "summary": "Get chat personal model override settings",
+ "operationId": "get-chat-personal-model-override-settings",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatPersonalModelOverridesAdminSettings"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
},
- "patch": {
+ "put": {
"consumes": [
"application/json"
],
- "produces": [
- "application/json"
- ],
"tags": [
"Chats"
],
- "summary": "Update an AI model",
- "operationId": "update-ai-model",
+ "summary": "Update chat personal model override settings",
+ "operationId": "update-chat-personal-model-override-settings",
"parameters": [
{
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "Model ID",
- "name": "model",
- "in": "path",
- "required": true
- },
- {
- "description": "Model updates",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateChatModelRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatPersonalModelOverridesAdminSettingsRequest"
}
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.ChatModel"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/chats/models/{model}/acl": {
+ "/api/v2/chats/config/plan-mode-instructions": {
"get": {
"produces": [
"application/json"
@@ -1685,30 +1736,13 @@ const docTemplate = `{
"tags": [
"Chats"
],
- "summary": "Get an AI model ACL",
- "operationId": "get-ai-model-acl",
- "parameters": [
- {
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "Model ID",
- "name": "model",
- "in": "path",
- "required": true
- }
- ],
+ "summary": "Get chat plan mode instructions",
+ "operationId": "get-chat-plan-mode-instructions",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatModelACL"
+ "$ref": "#/definitions/codersdk.ChatPlanModeInstructionsResponse"
}
}
},
@@ -1716,43 +1750,25 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
- "patch": {
+ "put": {
"consumes": [
"application/json"
],
"tags": [
"Chats"
],
- "summary": "Update an AI model ACL",
- "operationId": "update-ai-model-acl",
+ "summary": "Update chat plan mode instructions",
+ "operationId": "update-chat-plan-mode-instructions",
"parameters": [
{
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "Model ID",
- "name": "model",
- "in": "path",
- "required": true
- },
- {
- "description": "Sparse model ACL update",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateChatModelACLRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatPlanModeInstructionsRequest"
}
}
],
@@ -1765,40 +1781,24 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/mcp-servers": {
+ "/api/v2/chats/config/retention-days": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "MCP"
- ],
- "summary": "List MCP server configs",
- "operationId": "list-mcp-server-configs",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- }
+ "Chats"
],
+ "summary": "Get chat retention days",
+ "operationId": "get-chat-retention-days",
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.MCPServerConfig"
- }
+ "$ref": "#/definitions/codersdk.ChatRetentionDaysResponse"
}
}
},
@@ -1811,43 +1811,29 @@ const docTemplate = `{
"skip": true
}
},
- "post": {
+ "put": {
"consumes": [
"application/json"
],
- "produces": [
- "application/json"
- ],
"tags": [
- "MCP"
+ "Chats"
],
- "summary": "Create MCP server config",
- "operationId": "create-mcp-server-config",
+ "summary": "Update chat retention days",
+ "operationId": "update-chat-retention-days",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "description": "Create MCP server config request",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateMCPServerConfigRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatRetentionDaysRequest"
}
}
],
"responses": {
- "201": {
- "description": "Created",
- "schema": {
- "$ref": "#/definitions/codersdk.MCPServerConfig"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
@@ -1860,39 +1846,21 @@ const docTemplate = `{
}
}
},
- "/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}": {
+ "/api/v2/chats/config/system-prompt": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "MCP"
- ],
- "summary": "Get MCP server config",
- "operationId": "get-mcp-server-config",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
- "in": "path",
- "required": true
- }
+ "Chats"
],
+ "summary": "Get chat system prompt",
+ "operationId": "get-chat-system-prompt",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.MCPServerConfig"
+ "$ref": "#/definitions/codersdk.ChatSystemPromptResponse"
}
}
},
@@ -1900,33 +1868,26 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
- "delete": {
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
"tags": [
- "MCP"
+ "Chats"
],
- "summary": "Delete MCP server config",
- "operationId": "delete-mcp-server-config",
+ "summary": "Update chat system prompt",
+ "operationId": "update-chat-system-prompt",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
- "in": "path",
- "required": true
+ "description": "Request body",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateChatSystemPromptRequest"
+ }
}
],
"responses": {
@@ -1938,55 +1899,24 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
- },
- "patch": {
- "consumes": [
- "application/json"
- ],
+ ]
+ }
+ },
+ "/api/v2/chats/config/user-compaction-thresholds": {
+ "get": {
"produces": [
"application/json"
],
"tags": [
- "MCP"
- ],
- "summary": "Update MCP server config",
- "operationId": "update-mcp-server-config",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
- "in": "path",
- "required": true
- },
- {
- "description": "Update MCP server config request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.UpdateMCPServerConfigRequest"
- }
- }
+ "Chats"
],
+ "summary": "Get user chat compaction thresholds",
+ "operationId": "get-user-chat-compaction-thresholds",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.MCPServerConfig"
+ "$ref": "#/definitions/codersdk.UserChatCompactionThresholds"
}
}
},
@@ -2000,39 +1930,43 @@ const docTemplate = `{
}
}
},
- "/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl": {
- "get": {
+ "/api/v2/chats/config/user-compaction-thresholds/{modelConfig}": {
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
"produces": [
"application/json"
],
"tags": [
- "MCP"
+ "Chats"
],
- "summary": "Get MCP server config ACL",
- "operationId": "get-mcp-server-config-acl",
+ "summary": "Update user chat compaction threshold",
+ "operationId": "update-user-chat-compaction-threshold",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "Organization ID",
- "name": "organization",
+ "description": "Model config ID",
+ "name": "modelConfig",
"in": "path",
"required": true
},
{
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
- "in": "path",
- "required": true
+ "description": "Request body",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateUserChatCompactionThresholdRequest"
+ }
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.MCPServerConfigACL"
+ "$ref": "#/definitions/codersdk.UserChatCompactionThreshold"
}
}
},
@@ -2045,40 +1979,20 @@ const docTemplate = `{
"skip": true
}
},
- "patch": {
- "consumes": [
- "application/json"
- ],
+ "delete": {
"tags": [
- "MCP"
+ "Chats"
],
- "summary": "Update MCP server config ACL",
- "operationId": "update-mcp-server-config-acl",
+ "summary": "Delete user chat compaction threshold",
+ "operationId": "delete-user-chat-compaction-threshold",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
+ "description": "Model config ID",
+ "name": "modelConfig",
"in": "path",
"required": true
- },
- {
- "description": "Update MCP server config ACL request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.UpdateMCPServerConfigACLRequest"
- }
}
],
"responses": {
@@ -2096,47 +2010,7 @@ const docTemplate = `{
}
}
},
- "/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/oauth2/connect": {
- "get": {
- "tags": [
- "MCP"
- ],
- "summary": "Initiate MCP server OAuth2 connect",
- "operationId": "initiate-mcp-server-oauth2-connect",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
- "in": "path",
- "required": true
- }
- ],
- "responses": {
- "307": {
- "description": "Temporary Redirect"
- }
- },
- "security": [
- {
- "CoderSessionToken": []
- }
- ],
- "x-apidocgen": {
- "skip": true
- }
- }
- },
- "/api/experimental/organizations/{organization}/members/{user}/chats/model-overrides": {
+ "/api/v2/chats/config/user-debug-logging": {
"get": {
"produces": [
"application/json"
@@ -2144,29 +2018,13 @@ const docTemplate = `{
"tags": [
"Chats"
],
- "summary": "Get organization member chat model overrides",
- "operationId": "get-organization-member-chat-model-overrides",
- "parameters": [
- {
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "User name, ID, or me",
- "name": "user",
- "in": "path",
- "required": true
- }
- ],
+ "summary": "Get user chat debug logging setting",
+ "operationId": "get-user-chat-debug-logging-setting",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.UserChatPersonalModelOverridesResponse"
+ "$ref": "#/definitions/codersdk.UserChatDebugLoggingSettings"
}
}
},
@@ -2174,13 +2032,8 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
- }
- },
- "/api/experimental/organizations/{organization}/members/{user}/chats/model-overrides/{context}": {
+ ]
+ },
"put": {
"consumes": [
"application/json"
@@ -2188,42 +2041,16 @@ const docTemplate = `{
"tags": [
"Chats"
],
- "summary": "Update organization member chat model override",
- "operationId": "update-organization-member-chat-model-override",
+ "summary": "Update user chat debug logging setting",
+ "operationId": "update-user-chat-debug-logging-setting",
"parameters": [
{
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "User name, ID, or me",
- "name": "user",
- "in": "path",
- "required": true
- },
- {
- "enum": [
- "root",
- "general",
- "explore"
- ],
- "type": "string",
- "description": "Override context",
- "name": "context",
- "in": "path",
- "required": true
- },
- {
- "description": "Personal model override",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateUserChatPersonalModelOverrideRequest"
+ "$ref": "#/definitions/codersdk.UpdateUserChatDebugLoggingRequest"
}
}
],
@@ -2236,39 +2063,24 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/users/{user}/skills": {
+ "/api/v2/chats/config/user-prompt": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Users"
- ],
- "summary": "List user skills",
- "operationId": "list-user-skills",
- "parameters": [
- {
- "type": "string",
- "description": "User ID, username, or me",
- "name": "user",
- "in": "path",
- "required": true
- }
+ "Chats"
],
+ "summary": "Get user chat custom prompt",
+ "operationId": "get-user-chat-custom-prompt",
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.UserSkillMetadata"
- }
+ "$ref": "#/definitions/codersdk.UserChatCustomPrompt"
}
}
},
@@ -2276,12 +2088,9 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
- "post": {
+ "put": {
"consumes": [
"application/json"
],
@@ -2289,33 +2098,26 @@ const docTemplate = `{
"application/json"
],
"tags": [
- "Users"
+ "Chats"
],
- "summary": "Create a user skill",
- "operationId": "create-a-user-skill",
+ "summary": "Update user chat custom prompt",
+ "operationId": "update-user-chat-custom-prompt",
"parameters": [
{
- "type": "string",
- "description": "User ID, username, or me",
- "name": "user",
- "in": "path",
- "required": true
- },
- {
- "description": "Create user skill request",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateUserSkillRequest"
+ "$ref": "#/definitions/codersdk.UserChatCustomPrompt"
}
}
],
"responses": {
- "201": {
- "description": "Created",
+ "200": {
+ "description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.UserSkill"
+ "$ref": "#/definitions/codersdk.UserChatCustomPrompt"
}
}
},
@@ -2323,43 +2125,24 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/users/{user}/skills/{skillName}": {
+ "/api/v2/chats/config/workspace-ttl": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Users"
- ],
- "summary": "Get a user skill by name",
- "operationId": "get-a-user-skill-by-name",
- "parameters": [
- {
- "type": "string",
- "description": "User ID, username, or me",
- "name": "user",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "Skill name",
- "name": "skillName",
- "in": "path",
- "required": true
- }
+ "Chats"
],
+ "summary": "Get chat workspace time to live",
+ "operationId": "get-chat-workspace-time-to-live",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.UserSkill"
+ "$ref": "#/definitions/codersdk.ChatWorkspaceTTLResponse"
}
}
},
@@ -2367,31 +2150,26 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
- "delete": {
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
"tags": [
- "Users"
+ "Chats"
],
- "summary": "Delete a user skill",
- "operationId": "delete-a-user-skill",
+ "summary": "Update chat workspace time to live",
+ "operationId": "update-chat-workspace-time-to-live",
"parameters": [
{
- "type": "string",
- "description": "User ID, username, or me",
- "name": "user",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "Skill name",
- "name": "skillName",
- "in": "path",
- "required": true
+ "description": "Request body",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateChatWorkspaceTTLRequest"
+ }
}
],
"responses": {
@@ -2403,53 +2181,68 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
- },
- "patch": {
+ ]
+ }
+ },
+ "/api/v2/chats/files": {
+ "post": {
"consumes": [
- "application/json"
+ "image/png",
+ "image/jpeg",
+ "image/gif",
+ "image/webp",
+ "text/plain",
+ "text/markdown",
+ "text/csv",
+ "application/json",
+ "application/pdf"
],
"produces": [
"application/json"
],
"tags": [
- "Users"
+ "Chats"
],
- "summary": "Update a user skill",
- "operationId": "update-a-user-skill",
+ "summary": "Upload chat file",
+ "operationId": "upload-chat-file",
"parameters": [
{
"type": "string",
- "description": "User ID, username, or me",
- "name": "user",
- "in": "path",
+ "format": "uuid",
+ "description": "Organization ID",
+ "name": "organization",
+ "in": "query",
"required": true
},
{
"type": "string",
- "description": "Skill name",
- "name": "skillName",
- "in": "path",
+ "example": "attachment; filename=\"image.png\"",
+ "description": "Attachment disposition carrying the file name",
+ "name": "Content-Disposition",
+ "in": "header",
"required": true
},
{
- "description": "Update user skill request",
+ "description": "Raw file binary data",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateUserSkillRequest"
+ "type": "string"
}
}
],
"responses": {
- "200": {
- "description": "OK",
+ "201": {
+ "description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.UserSkill"
+ "$ref": "#/definitions/codersdk.UploadChatFileResponse"
+ }
+ },
+ "413": {
+ "description": "Request body exceeds 10 MiB",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
}
}
},
@@ -2459,133 +2252,120 @@ const docTemplate = `{
}
],
"x-apidocgen": {
- "skip": true
+ "rawBodyFile": "image.png"
}
}
},
- "/api/experimental/watch-all-workspacebuilds": {
+ "/api/v2/chats/files/{file}": {
"get": {
"produces": [
- "application/json"
+ "image/png",
+ "image/jpeg",
+ "image/gif",
+ "image/webp",
+ "text/plain",
+ "text/markdown",
+ "text/csv",
+ "application/json",
+ "application/pdf"
],
"tags": [
- "Workspaces"
+ "Chats"
+ ],
+ "summary": "Get chat file",
+ "operationId": "get-chat-file",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "File ID",
+ "name": "file",
+ "in": "path",
+ "required": true
+ }
],
- "summary": "Watch all workspace builds",
- "operationId": "watch-all-workspace-builds",
"responses": {
- "101": {
- "description": "Switching Protocols"
+ "200": {
+ "description": "OK"
}
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
- }
- },
- "/api/v2/": {
- "get": {
- "produces": [
- "application/json"
- ],
- "tags": [
- "General"
- ],
- "summary": "API root handler",
- "operationId": "api-root-handler",
- "responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.Response"
- }
- }
- }
+ ]
}
},
- "/api/v2/agent-firewall/sessions/{id}": {
+ "/api/v2/chats/files/{file}/download": {
"get": {
"produces": [
- "application/json"
+ "image/png",
+ "image/jpeg",
+ "image/gif",
+ "image/webp",
+ "text/plain",
+ "text/markdown",
+ "text/csv",
+ "application/json",
+ "application/pdf"
],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Get agent firewall session by ID",
- "operationId": "get-agent-firewall-session-by-id",
+ "summary": "Download chat file with signed token",
+ "operationId": "download-chat-file-with-signed-token",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "Agent firewall session ID",
- "name": "id",
+ "description": "File ID",
+ "name": "file",
"in": "path",
"required": true
+ },
+ {
+ "type": "string",
+ "description": "Signed download token",
+ "name": "token",
+ "in": "query",
+ "required": true
}
],
"responses": {
"200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.AgentFirewallSession"
- }
+ "description": "OK"
}
},
- "security": [
- {
- "CoderSessionToken": []
- }
- ]
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/agent-firewall/sessions/{id}/logs": {
- "get": {
+ "/api/v2/chats/files/{file}/download-url": {
+ "post": {
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Get agent firewall session logs",
- "operationId": "get-agent-firewall-session-logs",
+ "summary": "Create chat file download URL",
+ "operationId": "create-chat-file-download-url",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "Agent firewall session ID",
- "name": "id",
+ "description": "File ID",
+ "name": "file",
"in": "path",
"required": true
- },
- {
- "type": "integer",
- "description": "Inclusive lower bound on sequence number",
- "name": "seq_after",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Exclusive upper bound on sequence number",
- "name": "seq_before",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Maximum number of logs to return (default 100)",
- "name": "limit",
- "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AgentFirewallSessionLogsResponse"
+ "$ref": "#/definitions/codersdk.ChatFileDownloadURLResponse"
}
}
},
@@ -2593,28 +2373,27 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/ai-gateway/clients": {
+ "/api/v2/chats/watch": {
"get": {
- "description": "Alias: also available at /api/v2/aibridge/clients for backward compatibility.",
"produces": [
"application/json"
],
"tags": [
- "AI Gateway"
+ "Chats"
],
- "summary": "List AI Gateway clients",
- "operationId": "list-ai-gateway-clients",
+ "summary": "Watch chat events for a user via WebSockets",
+ "operationId": "watch-chat-events-for-a-user-via-websockets",
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "$ref": "#/definitions/codersdk.ChatWatchEvent"
}
}
},
@@ -2625,24 +2404,31 @@ const docTemplate = `{
]
}
},
- "/api/v2/ai-gateway/keys": {
+ "/api/v2/chats/{chat}": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "Chats"
+ ],
+ "summary": "Get chat by ID",
+ "operationId": "get-chat-by-id",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ }
],
- "summary": "List AI Gateway keys",
- "operationId": "list-ai-gateway-keys",
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.AIGatewayKey"
- }
+ "$ref": "#/definitions/codersdk.Chat"
}
}
},
@@ -2652,35 +2438,37 @@ const docTemplate = `{
}
]
},
- "post": {
+ "patch": {
"consumes": [
"application/json"
],
- "produces": [
- "application/json"
- ],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Create AI Gateway key",
- "operationId": "create-ai-gateway-key",
+ "summary": "Update chat",
+ "operationId": "update-chat",
"parameters": [
{
- "description": "Create AI Gateway key request",
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Update chat request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateAIGatewayKeyRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatRequest"
}
}
],
"responses": {
- "201": {
- "description": "Created",
- "schema": {
- "$ref": "#/definitions/codersdk.CreateAIGatewayKeyResponse"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
@@ -2690,125 +2478,112 @@ const docTemplate = `{
]
}
},
- "/api/v2/ai-gateway/keys/{key}": {
- "delete": {
+ "/api/v2/chats/{chat}/acl": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Delete AI Gateway key",
- "operationId": "delete-ai-gateway-key",
+ "summary": "Get chat ACLs",
+ "operationId": "get-chat-acls",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "Key ID",
- "name": "key",
+ "description": "Chat ID",
+ "name": "chat",
"in": "path",
"required": true
}
],
"responses": {
- "204": {
- "description": "No Content"
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatACL"
+ }
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
- }
- },
- "/api/v2/ai-gateway/models": {
- "get": {
- "description": "Alias: also available at /api/v2/aibridge/models for backward compatibility.",
- "produces": [
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "patch": {
+ "consumes": [
"application/json"
],
"tags": [
- "AI Gateway"
+ "Chats"
],
- "summary": "List AI Gateway models",
- "operationId": "list-ai-gateway-models",
- "responses": {
- "200": {
- "description": "OK",
+ "summary": "Update chat ACL",
+ "operationId": "update-chat-acl",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Update chat ACL request",
+ "name": "request",
+ "in": "body",
+ "required": true,
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "$ref": "#/definitions/codersdk.UpdateChatACL"
}
}
- },
- "security": [
- {
- "CoderSessionToken": []
- }
- ]
- }
- },
- "/api/v2/ai-gateway/serve": {
- "get": {
- "tags": [
- "Enterprise"
],
- "summary": "AI Gateway serve",
- "operationId": "ai-gateway-serve",
"responses": {
- "101": {
- "description": "Switching Protocols"
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
- "AIGatewayKey": []
+ "CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/ai-gateway/sessions": {
- "get": {
- "description": "Alias: also available at /api/v2/aibridge/sessions for backward compatibility.",
+ "/api/v2/chats/{chat}/compact": {
+ "post": {
+ "description": "Requests a manual context compaction on an idle or errored\nchat, clearing any stored error. The compaction runs\nasynchronously through the chat worker and bypasses the\nautomatic usage threshold.",
"produces": [
"application/json"
],
"tags": [
- "AI Gateway"
+ "Chats"
],
- "summary": "List AI Gateway sessions",
- "operationId": "list-ai-gateway-sessions",
+ "summary": "Compact chat",
+ "operationId": "compact-chat",
"parameters": [
{
"type": "string",
- "description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before.",
- "name": "q",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Page limit",
- "name": "limit",
- "in": "query"
- },
- {
- "type": "string",
- "description": "Cursor pagination after session ID (cannot be used with offset)",
- "name": "after_session_id",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Offset pagination (cannot be used with after_session_id)",
- "name": "offset",
- "in": "query"
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AIBridgeListSessionsResponse"
+ "$ref": "#/definitions/codersdk.Chat"
}
}
},
@@ -2816,52 +2591,37 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/ai-gateway/sessions/{session_id}": {
- "get": {
- "description": "Alias: also available at /api/v2/aibridge/sessions/{session_id} for backward compatibility.",
+ "/api/v2/chats/{chat}/context": {
+ "put": {
"produces": [
"application/json"
],
"tags": [
- "AI Gateway"
+ "Chats"
],
- "summary": "Get AI Gateway session threads",
- "operationId": "get-ai-gateway-session-threads",
+ "summary": "Refresh chat context",
+ "operationId": "refresh-chat-context",
"parameters": [
{
"type": "string",
- "description": "Session ID (client_session_id or interception UUID)",
- "name": "session_id",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
"in": "path",
"required": true
- },
- {
- "type": "string",
- "description": "Thread pagination cursor (forward/older)",
- "name": "after_id",
- "in": "query"
- },
- {
- "type": "string",
- "description": "Thread pagination cursor (backward/newer)",
- "name": "before_id",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Number of threads per page (default 50)",
- "name": "limit",
- "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AIBridgeSessionThreadsResponse"
+ "$ref": "#/definitions/codersdk.Chat"
}
}
},
@@ -2872,24 +2632,32 @@ const docTemplate = `{
]
}
},
- "/api/v2/ai/providers": {
+ "/api/v2/chats/{chat}/cost": {
"get": {
+ "description": "Cost covers the whole chat tree: the root chat plus every\nsubagent chat beneath it. Requesting cost for a subagent chat\nreturns that same total.\n\nCost is derived from AI Gateway data, which is subject to its\nown retention period, 60 days by default, configured\nindependently of chat retention. Spend for requests older than\nthat period is no longer reported, so a chat whose requests\nhave all been purged reports zero cost.",
"produces": [
"application/json"
],
"tags": [
- "AI Providers"
+ "Chats"
+ ],
+ "summary": "Get chat cost",
+ "operationId": "get-chat-cost",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ }
],
- "summary": "List AI providers",
- "operationId": "list-ai-providers",
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.AIProvider"
- }
+ "$ref": "#/definitions/codersdk.ChatCost"
}
}
},
@@ -2898,35 +2666,33 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- },
- "post": {
- "consumes": [
- "application/json"
- ],
+ }
+ },
+ "/api/v2/chats/{chat}/diff": {
+ "get": {
"produces": [
"application/json"
],
"tags": [
- "AI Providers"
+ "Chats"
],
- "summary": "Create an AI provider",
- "operationId": "create-an-ai-provider",
+ "summary": "Get chat diff contents",
+ "operationId": "get-chat-diff-contents",
"parameters": [
{
- "description": "Create AI provider request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.CreateAIProviderRequest"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
}
],
"responses": {
- "201": {
- "description": "Created",
+ "200": {
+ "description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AIProvider"
+ "$ref": "#/definitions/codersdk.ChatDiffContents"
}
}
},
@@ -2937,21 +2703,22 @@ const docTemplate = `{
]
}
},
- "/api/v2/ai/providers/{idOrName}": {
- "get": {
+ "/api/v2/chats/{chat}/interrupt": {
+ "post": {
"produces": [
"application/json"
],
"tags": [
- "AI Providers"
+ "Chats"
],
- "summary": "Get an AI provider",
- "operationId": "get-an-ai-provider",
+ "summary": "Interrupt chat",
+ "operationId": "interrupt-chat",
"parameters": [
{
"type": "string",
- "description": "Provider ID or name",
- "name": "idOrName",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
"in": "path",
"required": true
}
@@ -2960,7 +2727,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AIProvider"
+ "$ref": "#/definitions/codersdk.Chat"
}
}
},
@@ -2969,25 +2736,52 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- },
- "delete": {
+ }
+ },
+ "/api/v2/chats/{chat}/messages": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
"tags": [
- "AI Providers"
+ "Chats"
],
- "summary": "Delete an AI provider",
- "operationId": "delete-an-ai-provider",
+ "summary": "List chat messages",
+ "operationId": "list-chat-messages",
"parameters": [
{
"type": "string",
- "description": "Provider ID or name",
- "name": "idOrName",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
"in": "path",
"required": true
+ },
+ {
+ "type": "integer",
+ "description": "Return messages with id \u003c before_id",
+ "name": "before_id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Return messages with id \u003e after_id",
+ "name": "after_id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page size, 1 to 200. Defaults to 50.",
+ "name": "limit",
+ "in": "query"
}
],
"responses": {
- "204": {
- "description": "No Content"
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatMessagesResponse"
+ }
}
},
"security": [
@@ -2996,7 +2790,7 @@ const docTemplate = `{
}
]
},
- "patch": {
+ "post": {
"consumes": [
"application/json"
],
@@ -3004,25 +2798,26 @@ const docTemplate = `{
"application/json"
],
"tags": [
- "AI Providers"
+ "Chats"
],
- "summary": "Update an AI provider",
- "operationId": "update-an-ai-provider",
+ "summary": "Send chat message",
+ "operationId": "send-chat-message",
"parameters": [
{
"type": "string",
- "description": "Provider ID or name",
- "name": "idOrName",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
"in": "path",
"required": true
},
{
- "description": "Update AI provider request",
+ "description": "Create chat message request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateAIProviderRequest"
+ "$ref": "#/definitions/codersdk.CreateChatMessageRequest"
}
}
],
@@ -3030,7 +2825,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AIProvider"
+ "$ref": "#/definitions/codersdk.CreateChatMessageResponse"
}
}
},
@@ -3041,21 +2836,50 @@ const docTemplate = `{
]
}
},
- "/api/v2/appearance": {
- "get": {
+ "/api/v2/chats/{chat}/messages/{message}": {
+ "patch": {
+ "consumes": [
+ "application/json"
+ ],
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "Chats"
+ ],
+ "summary": "Edit chat message",
+ "operationId": "edit-chat-message",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "Message ID",
+ "name": "message",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Edit chat message request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.EditChatMessageRequest"
+ }
+ }
],
- "summary": "Get appearance",
- "operationId": "get-appearance",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AppearanceConfig"
+ "$ref": "#/definitions/codersdk.EditChatMessageResponse"
}
}
},
@@ -3064,35 +2888,40 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- },
- "put": {
- "consumes": [
- "application/json"
- ],
+ }
+ },
+ "/api/v2/chats/{chat}/prompts": {
+ "get": {
+ "description": "Returns the user-authored prompts in a chat, newest first,\nwith each prompt's text parts concatenated in the order they\nwere authored. Used by the composer to power the up/down\narrow prompt-history cycle without paging through every\nmessage in the chat.",
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Update appearance",
- "operationId": "update-appearance",
+ "summary": "List chat user prompts",
+ "operationId": "list-chat-user-prompts",
"parameters": [
{
- "description": "Update appearance request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.UpdateAppearanceConfig"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "Page size, 0 to 2000. 0 (the default) means the server-side default of 500.",
+ "name": "limit",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.UpdateAppearanceConfig"
+ "$ref": "#/definitions/codersdk.ChatPromptsResponse"
}
}
},
@@ -3103,24 +2932,33 @@ const docTemplate = `{
]
}
},
- "/api/v2/applications/auth-redirect": {
- "get": {
+ "/api/v2/chats/{chat}/queue/{queuedMessage}": {
+ "delete": {
"tags": [
- "Applications"
+ "Chats"
],
- "summary": "Redirect to URI with encrypted API key",
- "operationId": "redirect-to-uri-with-encrypted-api-key",
+ "summary": "Delete chat queued message",
+ "operationId": "delete-chat-queued-message",
"parameters": [
{
"type": "string",
- "description": "Redirect destination",
- "name": "redirect_uri",
- "in": "query"
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "Queued message ID",
+ "name": "queuedMessage",
+ "in": "path",
+ "required": true
}
],
"responses": {
- "307": {
- "description": "Temporary Redirect"
+ "204": {
+ "description": "No Content"
}
},
"security": [
@@ -3130,22 +2968,38 @@ const docTemplate = `{
]
}
},
- "/api/v2/applications/host": {
- "get": {
+ "/api/v2/chats/{chat}/queue/{queuedMessage}/promote": {
+ "post": {
"produces": [
"application/json"
],
"tags": [
- "Applications"
+ "Chats"
+ ],
+ "summary": "Promote chat queued message",
+ "operationId": "promote-chat-queued-message",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "Queued message ID",
+ "name": "queuedMessage",
+ "in": "path",
+ "required": true
+ }
],
- "summary": "Get applications host",
- "operationId": "get-applications-host",
- "deprecated": true,
"responses": {
- "200": {
- "description": "OK",
+ "202": {
+ "description": "Accepted",
"schema": {
- "$ref": "#/definitions/codersdk.AppHostResponse"
+ "$ref": "#/definitions/codersdk.Response"
}
}
},
@@ -3156,35 +3010,31 @@ const docTemplate = `{
]
}
},
- "/api/v2/applications/reconnecting-pty-signed-token": {
+ "/api/v2/chats/{chat}/reconcile-invalid": {
"post": {
- "consumes": [
- "application/json"
- ],
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Issue signed app token for reconnecting PTY",
- "operationId": "issue-signed-app-token-for-reconnecting-pty",
+ "summary": "Reconcile invalid chat state",
+ "operationId": "reconcile-invalid-chat-state",
"parameters": [
{
- "description": "Issue reconnecting PTY signed token request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.IssueReconnectingPTYSignedTokenRequest"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.IssueReconnectingPTYSignedTokenResponse"
+ "$ref": "#/definitions/codersdk.Chat"
}
}
},
@@ -3192,40 +3042,32 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/v2/audit": {
+ "/api/v2/chats/{chat}/stream": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Audit"
+ "Chats"
],
- "summary": "Get audit logs",
- "operationId": "get-audit-logs",
+ "summary": "Stream chat events via WebSockets",
+ "operationId": "stream-chat-events-via-websockets",
"parameters": [
{
"type": "string",
- "description": "Search query",
- "name": "q",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Page limit",
- "name": "limit",
- "in": "query",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
"required": true
},
{
"type": "integer",
- "description": "Page offset",
- "name": "offset",
+ "description": "Skip snapshot messages with id at or before this cursor",
+ "name": "after_id",
"in": "query"
}
],
@@ -3233,7 +3075,10 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AuditLogResponse"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.ChatStreamEvent"
+ }
}
}
},
@@ -3244,91 +3089,107 @@ const docTemplate = `{
]
}
},
- "/api/v2/audit/testgenerate": {
- "post": {
- "consumes": [
+ "/api/v2/chats/{chat}/stream/git": {
+ "get": {
+ "produces": [
"application/json"
],
"tags": [
- "Audit"
+ "Chats"
],
- "summary": "Generate fake audit log",
- "operationId": "generate-fake-audit-log",
+ "summary": "Watch chat workspace git state via WebSockets",
+ "operationId": "watch-chat-workspace-git-state-via-websockets",
"parameters": [
{
- "description": "Audit log request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.CreateTestAuditLogRequest"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
}
],
"responses": {
- "204": {
- "description": "No Content"
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.WorkspaceAgentGitServerMessage"
+ }
}
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/v2/auth/scopes": {
+ "/api/v2/chats/{chat}/stream/parts": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Authorization"
+ "Chats"
+ ],
+ "summary": "Stream chat parts via WebSockets",
+ "operationId": "stream-chat-parts-via-websockets",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ }
],
- "summary": "List API key scopes",
- "operationId": "list-api-key-scopes",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ExternalAPIKeyScopes"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.ChatStreamEvent"
+ }
}
}
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
}
}
},
- "/api/v2/authcheck": {
+ "/api/v2/chats/{chat}/title/propose": {
"post": {
- "consumes": [
- "application/json"
- ],
"produces": [
"application/json"
],
"tags": [
- "Authorization"
+ "Chats"
],
- "summary": "Check authorization",
- "operationId": "check-authorization",
+ "summary": "Propose chat title",
+ "operationId": "propose-chat-title",
"parameters": [
{
- "description": "Authorization request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.AuthorizationRequest"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AuthorizationResponse"
+ "$ref": "#/definitions/codersdk.ProposeChatTitleResponse"
}
}
},
@@ -3339,24 +3200,45 @@ const docTemplate = `{
]
}
},
- "/api/v2/buildinfo": {
- "get": {
- "produces": [
+ "/api/v2/chats/{chat}/tool-results": {
+ "post": {
+ "consumes": [
"application/json"
],
"tags": [
- "General"
+ "Chats"
],
- "summary": "Build info",
- "operationId": "build-info",
- "responses": {
- "200": {
- "description": "OK",
+ "summary": "Submit chat tool results",
+ "operationId": "submit-chat-tool-results",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Request body",
+ "name": "request",
+ "in": "body",
+ "required": true,
"schema": {
- "$ref": "#/definitions/codersdk.BuildInfoResponse"
+ "$ref": "#/definitions/codersdk.SubmitToolResultsRequest"
}
}
- }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
}
},
"/api/v2/connectionlog": {
@@ -4944,8 +4826,553 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.UserLatencyInsightsResponse"
+ "$ref": "#/definitions/codersdk.UserLatencyInsightsResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/insights/user-status-counts": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Insights"
+ ],
+ "summary": "Get insights about user status counts",
+ "operationId": "get-insights-about-user-status-counts",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "IANA timezone name (e.g. America/St_Johns)",
+ "name": "timezone",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Deprecated: Time-zone offset (e.g. -2). Use timezone instead.",
+ "name": "tz_offset",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.GetUserStatusCountsResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/licenses": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Enterprise"
+ ],
+ "summary": "Get licenses",
+ "operationId": "get-licenses",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.License"
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Enterprise"
+ ],
+ "summary": "Add new license",
+ "operationId": "add-new-license",
+ "parameters": [
+ {
+ "description": "Add license request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.AddLicenseRequest"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "$ref": "#/definitions/codersdk.License"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/licenses/refresh-entitlements": {
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Enterprise"
+ ],
+ "summary": "Update license entitlements",
+ "operationId": "update-license-entitlements",
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/licenses/trial": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Enterprise"
+ ],
+ "summary": "Request a trial license",
+ "operationId": "request-a-trial-license",
+ "parameters": [
+ {
+ "description": "Trial license request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateTrialLicenseRequest"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "$ref": "#/definitions/codersdk.License"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/licenses/{id}": {
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Enterprise"
+ ],
+ "summary": "Delete license",
+ "operationId": "delete-license",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "number",
+ "description": "License ID",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/mcp/servers/{mcpServer}/oauth2/disconnect": {
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "MCP"
+ ],
+ "summary": "Disconnect MCP server OAuth2 token",
+ "operationId": "disconnect-mcp-server-oauth2-token",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpServer",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.MCPServerOAuth2DisconnectResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/v2/notifications/custom": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Notifications"
+ ],
+ "summary": "Send a custom notification",
+ "operationId": "send-a-custom-notification",
+ "parameters": [
+ {
+ "description": "Provide a non-empty title or message",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CustomNotificationRequest"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ },
+ "400": {
+ "description": "Invalid request body",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
+ }
+ },
+ "403": {
+ "description": "System users cannot send custom notifications",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
+ }
+ },
+ "500": {
+ "description": "Failed to send custom notification",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/notifications/dispatch-methods": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Notifications"
+ ],
+ "summary": "Get notification dispatch methods",
+ "operationId": "get-notification-dispatch-methods",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.NotificationMethodsResponse"
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/notifications/inbox": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Notifications"
+ ],
+ "summary": "List inbox notifications",
+ "operationId": "list-inbox-notifications",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Comma-separated list of target IDs to filter notifications",
+ "name": "targets",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Comma-separated list of template IDs to filter notifications",
+ "name": "templates",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Filter notifications by read status. Possible values: read, unread, all",
+ "name": "read_status",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "ID of the last notification from the current page. Notifications returned will be older than the associated one",
+ "name": "starting_before",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ListInboxNotificationsResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/notifications/inbox/mark-all-as-read": {
+ "put": {
+ "tags": [
+ "Notifications"
+ ],
+ "summary": "Mark all unread notifications as read",
+ "operationId": "mark-all-unread-notifications-as-read",
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/notifications/inbox/watch": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Notifications"
+ ],
+ "summary": "Watch for new inbox notifications",
+ "operationId": "watch-for-new-inbox-notifications",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Comma-separated list of target IDs to filter notifications",
+ "name": "targets",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Comma-separated list of template IDs to filter notifications",
+ "name": "templates",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Filter notifications by read status. Possible values: read, unread, all",
+ "name": "read_status",
+ "in": "query"
+ },
+ {
+ "enum": [
+ "plaintext",
+ "markdown"
+ ],
+ "type": "string",
+ "description": "Define the output format for notifications title and body.",
+ "name": "format",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.GetInboxNotificationResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/notifications/inbox/{id}/read-status": {
+ "put": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Notifications"
+ ],
+ "summary": "Update read status of a notification",
+ "operationId": "update-read-status-of-a-notification",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "id of the notification",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/notifications/settings": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Notifications"
+ ],
+ "summary": "Get notifications settings",
+ "operationId": "get-notifications-settings",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.NotificationsSettings"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Notifications"
+ ],
+ "summary": "Update notifications settings",
+ "operationId": "update-notifications-settings",
+ "parameters": [
+ {
+ "description": "Notifications settings request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.NotificationsSettings"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.NotificationsSettings"
}
+ },
+ "304": {
+ "description": "Not Modified"
}
},
"security": [
@@ -4955,35 +5382,30 @@ const docTemplate = `{
]
}
},
- "/api/v2/insights/user-status-counts": {
+ "/api/v2/notifications/templates/custom": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Insights"
- ],
- "summary": "Get insights about user status counts",
- "operationId": "get-insights-about-user-status-counts",
- "parameters": [
- {
- "type": "string",
- "description": "IANA timezone name (e.g. America/St_Johns)",
- "name": "timezone",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Deprecated: Time-zone offset (e.g. -2). Use timezone instead.",
- "name": "tz_offset",
- "in": "query"
- }
+ "Notifications"
],
+ "summary": "Get custom notification templates",
+ "operationId": "get-custom-notification-templates",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.GetUserStatusCountsResponse"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.NotificationTemplate"
+ }
+ }
+ },
+ "500": {
+ "description": "Failed to retrieve 'custom' notifications template",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
}
}
},
@@ -4994,25 +5416,31 @@ const docTemplate = `{
]
}
},
- "/api/v2/licenses": {
+ "/api/v2/notifications/templates/system": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "Notifications"
],
- "summary": "Get licenses",
- "operationId": "get-licenses",
+ "summary": "Get system notification templates",
+ "operationId": "get-system-notification-templates",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/codersdk.License"
+ "$ref": "#/definitions/codersdk.NotificationTemplate"
}
}
+ },
+ "500": {
+ "description": "Failed to retrieve 'system' notifications template",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
+ }
}
},
"security": [
@@ -5020,36 +5448,33 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- },
- "post": {
- "consumes": [
- "application/json"
- ],
+ }
+ },
+ "/api/v2/notifications/templates/{notification_template}/method": {
+ "put": {
"produces": [
"application/json"
],
"tags": [
"Enterprise"
],
- "summary": "Add new license",
- "operationId": "add-new-license",
+ "summary": "Update notification template dispatch method",
+ "operationId": "update-notification-template-dispatch-method",
"parameters": [
{
- "description": "Add license request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.AddLicenseRequest"
- }
+ "type": "string",
+ "description": "Notification template UUID",
+ "name": "notification_template",
+ "in": "path",
+ "required": true
}
],
"responses": {
- "201": {
- "description": "Created",
- "schema": {
- "$ref": "#/definitions/codersdk.License"
- }
+ "200": {
+ "description": "Success"
+ },
+ "304": {
+ "description": "Not modified"
}
},
"security": [
@@ -5059,21 +5484,51 @@ const docTemplate = `{
]
}
},
- "/api/v2/licenses/refresh-entitlements": {
+ "/api/v2/notifications/test": {
"post": {
+ "tags": [
+ "Notifications"
+ ],
+ "summary": "Send a test notification",
+ "operationId": "send-a-test-notification",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/oauth2-provider/apps": {
+ "get": {
"produces": [
"application/json"
],
"tags": [
"Enterprise"
],
- "summary": "Update license entitlements",
- "operationId": "update-license-entitlements",
+ "summary": "Get OAuth2 applications.",
+ "operationId": "get-oauth2-applications",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Filter by applications authorized for a user",
+ "name": "user_id",
+ "in": "query"
+ }
+ ],
"responses": {
- "201": {
- "description": "Created",
+ "200": {
+ "description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.OAuth2ProviderApp"
+ }
}
}
},
@@ -5082,9 +5537,7 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- }
- },
- "/api/v2/licenses/trial": {
+ },
"post": {
"consumes": [
"application/json"
@@ -5095,24 +5548,24 @@ const docTemplate = `{
"tags": [
"Enterprise"
],
- "summary": "Request a trial license",
- "operationId": "request-a-trial-license",
+ "summary": "Create OAuth2 application.",
+ "operationId": "create-oauth2-application",
"parameters": [
{
- "description": "Trial license request",
+ "description": "The OAuth2 application to create.",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateTrialLicenseRequest"
+ "$ref": "#/definitions/codersdk.PostOAuth2ProviderAppRequest"
}
}
],
"responses": {
- "201": {
- "description": "Created",
+ "200": {
+ "description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.License"
+ "$ref": "#/definitions/codersdk.OAuth2ProviderApp"
}
}
},
@@ -5123,29 +5576,31 @@ const docTemplate = `{
]
}
},
- "/api/v2/licenses/{id}": {
- "delete": {
+ "/api/v2/oauth2-provider/apps/{app}": {
+ "get": {
"produces": [
"application/json"
],
"tags": [
"Enterprise"
],
- "summary": "Delete license",
- "operationId": "delete-license",
+ "summary": "Get OAuth2 application.",
+ "operationId": "get-oauth2-application",
"parameters": [
{
"type": "string",
- "format": "number",
- "description": "License ID",
- "name": "id",
+ "description": "App ID",
+ "name": "app",
"in": "path",
"required": true
}
],
"responses": {
"200": {
- "description": "OK"
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.OAuth2ProviderApp"
+ }
}
},
"security": [
@@ -5153,10 +5608,8 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- }
- },
- "/api/v2/notifications/custom": {
- "post": {
+ },
+ "put": {
"consumes": [
"application/json"
],
@@ -5164,41 +5617,33 @@ const docTemplate = `{
"application/json"
],
"tags": [
- "Notifications"
+ "Enterprise"
],
- "summary": "Send a custom notification",
- "operationId": "send-a-custom-notification",
+ "summary": "Update OAuth2 application.",
+ "operationId": "update-oauth2-application",
"parameters": [
{
- "description": "Provide a non-empty title or message",
+ "type": "string",
+ "description": "App ID",
+ "name": "app",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Update an OAuth2 application.",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CustomNotificationRequest"
+ "$ref": "#/definitions/codersdk.PutOAuth2ProviderAppRequest"
}
}
],
"responses": {
- "204": {
- "description": "No Content"
- },
- "400": {
- "description": "Invalid request body",
- "schema": {
- "$ref": "#/definitions/codersdk.Response"
- }
- },
- "403": {
- "description": "System users cannot send custom notifications",
- "schema": {
- "$ref": "#/definitions/codersdk.Response"
- }
- },
- "500": {
- "description": "Failed to send custom notification",
+ "200": {
+ "description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "$ref": "#/definitions/codersdk.OAuth2ProviderApp"
}
}
},
@@ -5207,25 +5652,60 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
+ },
+ "delete": {
+ "tags": [
+ "Enterprise"
+ ],
+ "summary": "Delete OAuth2 application.",
+ "operationId": "delete-oauth2-application",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "App ID",
+ "name": "app",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
}
},
- "/api/v2/notifications/dispatch-methods": {
+ "/api/v2/oauth2-provider/apps/{app}/secrets": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Notifications"
+ "Enterprise"
+ ],
+ "summary": "Get OAuth2 application secrets.",
+ "operationId": "get-oauth2-application-secrets",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "App ID",
+ "name": "app",
+ "in": "path",
+ "required": true
+ }
],
- "summary": "Get notification dispatch methods",
- "operationId": "get-notification-dispatch-methods",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/codersdk.NotificationMethodsResponse"
+ "$ref": "#/definitions/codersdk.OAuth2ProviderAppSecret"
}
}
}
@@ -5235,50 +5715,33 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- }
- },
- "/api/v2/notifications/inbox": {
- "get": {
+ },
+ "post": {
"produces": [
"application/json"
],
"tags": [
- "Notifications"
+ "Enterprise"
],
- "summary": "List inbox notifications",
- "operationId": "list-inbox-notifications",
+ "summary": "Create OAuth2 application secret.",
+ "operationId": "create-oauth2-application-secret",
"parameters": [
{
"type": "string",
- "description": "Comma-separated list of target IDs to filter notifications",
- "name": "targets",
- "in": "query"
- },
- {
- "type": "string",
- "description": "Comma-separated list of template IDs to filter notifications",
- "name": "templates",
- "in": "query"
- },
- {
- "type": "string",
- "description": "Filter notifications by read status. Possible values: read, unread, all",
- "name": "read_status",
- "in": "query"
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "ID of the last notification from the current page. Notifications returned will be older than the associated one",
- "name": "starting_before",
- "in": "query"
+ "description": "App ID",
+ "name": "app",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ListInboxNotificationsResponse"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.OAuth2ProviderAppSecretFull"
+ }
}
}
},
@@ -5289,13 +5752,29 @@ const docTemplate = `{
]
}
},
- "/api/v2/notifications/inbox/mark-all-as-read": {
- "put": {
+ "/api/v2/oauth2-provider/apps/{app}/secrets/{secretID}": {
+ "delete": {
"tags": [
- "Notifications"
+ "Enterprise"
+ ],
+ "summary": "Delete OAuth2 application secret.",
+ "operationId": "delete-oauth2-application-secret",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "App ID",
+ "name": "app",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Secret ID",
+ "name": "secretID",
+ "in": "path",
+ "required": true
+ }
],
- "summary": "Mark all unread notifications as read",
- "operationId": "mark-all-unread-notifications-as-read",
"responses": {
"204": {
"description": "No Content"
@@ -5308,51 +5787,21 @@ const docTemplate = `{
]
}
},
- "/api/v2/notifications/inbox/watch": {
+ "/api/v2/oauth2-provider/settings": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Notifications"
- ],
- "summary": "Watch for new inbox notifications",
- "operationId": "watch-for-new-inbox-notifications",
- "parameters": [
- {
- "type": "string",
- "description": "Comma-separated list of target IDs to filter notifications",
- "name": "targets",
- "in": "query"
- },
- {
- "type": "string",
- "description": "Comma-separated list of template IDs to filter notifications",
- "name": "templates",
- "in": "query"
- },
- {
- "type": "string",
- "description": "Filter notifications by read status. Possible values: read, unread, all",
- "name": "read_status",
- "in": "query"
- },
- {
- "enum": [
- "plaintext",
- "markdown"
- ],
- "type": "string",
- "description": "Define the output format for notifications title and body.",
- "name": "format",
- "in": "query"
- }
+ "Enterprise"
],
+ "summary": "Get OAuth2 provider settings.",
+ "operationId": "get-oauth2-provider-settings",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.GetInboxNotificationResponse"
+ "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
}
}
},
@@ -5361,32 +5810,35 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- }
- },
- "/api/v2/notifications/inbox/{id}/read-status": {
+ },
"put": {
+ "consumes": [
+ "application/json"
+ ],
"produces": [
"application/json"
],
"tags": [
- "Notifications"
+ "Enterprise"
],
- "summary": "Update read status of a notification",
- "operationId": "update-read-status-of-a-notification",
+ "summary": "Update OAuth2 provider settings.",
+ "operationId": "update-oauth2-provider-settings",
"parameters": [
{
- "type": "string",
- "description": "id of the notification",
- "name": "id",
- "in": "path",
- "required": true
+ "description": "OAuth2 provider settings request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
+ }
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
}
}
},
@@ -5397,21 +5849,24 @@ const docTemplate = `{
]
}
},
- "/api/v2/notifications/settings": {
+ "/api/v2/organizations": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Notifications"
+ "Organizations"
],
- "summary": "Get notifications settings",
- "operationId": "get-notifications-settings",
+ "summary": "Get organizations",
+ "operationId": "get-organizations",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.NotificationsSettings"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.Organization"
+ }
}
}
},
@@ -5421,7 +5876,7 @@ const docTemplate = `{
}
]
},
- "put": {
+ "post": {
"consumes": [
"application/json"
],
@@ -5429,30 +5884,27 @@ const docTemplate = `{
"application/json"
],
"tags": [
- "Notifications"
+ "Organizations"
],
- "summary": "Update notifications settings",
- "operationId": "update-notifications-settings",
+ "summary": "Create organization",
+ "operationId": "create-organization",
"parameters": [
{
- "description": "Notifications settings request",
+ "description": "Create organization request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.NotificationsSettings"
+ "$ref": "#/definitions/codersdk.CreateOrganizationRequest"
}
}
],
"responses": {
- "200": {
- "description": "OK",
+ "201": {
+ "description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.NotificationsSettings"
+ "$ref": "#/definitions/codersdk.Organization"
}
- },
- "304": {
- "description": "Not Modified"
}
},
"security": [
@@ -5462,28 +5914,61 @@ const docTemplate = `{
]
}
},
- "/api/v2/notifications/templates/custom": {
+ "/api/v2/organizations/{organization}": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Notifications"
+ "Organizations"
+ ],
+ "summary": "Get organization by ID",
+ "operationId": "get-organization-by-id",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Organization ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ }
],
- "summary": "Get custom notification templates",
- "operationId": "get-custom-notification-templates",
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.NotificationTemplate"
- }
+ "$ref": "#/definitions/codersdk.Organization"
}
- },
- "500": {
- "description": "Failed to retrieve 'custom' notifications template",
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Organizations"
+ ],
+ "summary": "Delete organization",
+ "operationId": "delete-organization",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization ID or name",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
@@ -5494,32 +5979,42 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- }
- },
- "/api/v2/notifications/templates/system": {
- "get": {
+ },
+ "patch": {
+ "consumes": [
+ "application/json"
+ ],
"produces": [
"application/json"
],
"tags": [
- "Notifications"
+ "Organizations"
+ ],
+ "summary": "Update organization",
+ "operationId": "update-organization",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization ID or name",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Patch organization request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateOrganizationRequest"
+ }
+ }
],
- "summary": "Get system notification templates",
- "operationId": "get-system-notification-templates",
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.NotificationTemplate"
- }
- }
- },
- "500": {
- "description": "Failed to retrieve 'system' notifications template",
- "schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "$ref": "#/definitions/codersdk.Organization"
}
}
},
@@ -5530,47 +6025,41 @@ const docTemplate = `{
]
}
},
- "/api/v2/notifications/templates/{notification_template}/method": {
- "put": {
+ "/api/v2/organizations/{organization}/ai/spend/export": {
+ "get": {
+ "description": "Returns per-user, per-group, per-model, per-provider aggregated AI spend for the organization as CSV, built from raw AI Gateway token usage.\nThe optional period_start and period_end query parameters bound the period and are interpreted as UTC. They must be provided together and span at most 31 days. When both are omitted, the current UTC monthly period is used.\nAn explicit period_start must fall within the configured AI Gateway data retention window, since older token usage is purged. The default period is narrowed to that window instead, and every row echoes the applied bounds.\nRequires organization-level administrator permissions.",
"produces": [
- "application/json"
+ "text/csv"
],
"tags": [
"Enterprise"
],
- "summary": "Update notification template dispatch method",
- "operationId": "update-notification-template-dispatch-method",
+ "summary": "Export organization AI spend as CSV",
+ "operationId": "export-organization-ai-spend-as-csv",
"parameters": [
{
"type": "string",
- "description": "Notification template UUID",
- "name": "notification_template",
+ "format": "uuid",
+ "description": "Organization ID",
+ "name": "organization",
"in": "path",
"required": true
- }
- ],
- "responses": {
- "200": {
- "description": "Success"
},
- "304": {
- "description": "Not modified"
- }
- },
- "security": [
{
- "CoderSessionToken": []
+ "type": "string",
+ "format": "date-time",
+ "description": "Inclusive lower bound (RFC3339)",
+ "name": "period_start",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "format": "date-time",
+ "description": "Exclusive upper bound (RFC3339)",
+ "name": "period_end",
+ "in": "query"
}
- ]
- }
- },
- "/api/v2/notifications/test": {
- "post": {
- "tags": [
- "Notifications"
],
- "summary": "Send a test notification",
- "operationId": "send-a-test-notification",
"responses": {
"200": {
"description": "OK"
@@ -5583,32 +6072,30 @@ const docTemplate = `{
]
}
},
- "/api/v2/oauth2-provider/apps": {
+ "/api/v2/organizations/{organization}/chats/model-overrides": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Get OAuth2 applications.",
- "operationId": "get-oauth2-applications",
+ "summary": "List organization chat model overrides",
+ "operationId": "list-organization-chat-model-overrides",
"parameters": [
{
"type": "string",
- "description": "Filter by applications authorized for a user",
- "name": "user_id",
- "in": "query"
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderApp"
- }
+ "$ref": "#/definitions/codersdk.ChatModelOverridesResponse"
}
}
},
@@ -5616,9 +6103,14 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
- },
- "post": {
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/v2/organizations/{organization}/chats/model-overrides/{context}": {
+ "put": {
"consumes": [
"application/json"
],
@@ -5626,18 +6118,39 @@ const docTemplate = `{
"application/json"
],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Create OAuth2 application.",
- "operationId": "create-oauth2-application",
+ "summary": "Update organization chat model override",
+ "operationId": "update-organization-chat-model-override",
"parameters": [
{
- "description": "The OAuth2 application to create.",
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "enum": [
+ "general",
+ "explore",
+ "title_generation",
+ "compaction",
+ "advisor"
+ ],
+ "type": "string",
+ "description": "Override context",
+ "name": "context",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Model override",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.PostOAuth2ProviderAppRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatModelOverrideRequest"
}
}
],
@@ -5645,7 +6158,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderApp"
+ "$ref": "#/definitions/codersdk.ChatModelOverrideResponse"
}
}
},
@@ -5653,24 +6166,27 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/oauth2-provider/apps/{app}": {
+ "/api/v2/organizations/{organization}/chats/models": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Get OAuth2 application.",
- "operationId": "get-oauth2-application",
+ "summary": "List AI models and provider descriptors in an organization",
+ "operationId": "list-ai-models-and-provider-descriptors-in-an-organization",
"parameters": [
{
"type": "string",
- "description": "App ID",
- "name": "app",
+ "description": "Organization name or ID",
+ "name": "organization",
"in": "path",
"required": true
}
@@ -5679,7 +6195,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderApp"
+ "$ref": "#/definitions/codersdk.OrganizationChatModelsResponse"
}
}
},
@@ -5687,9 +6203,12 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
},
- "put": {
+ "post": {
"consumes": [
"application/json"
],
@@ -5697,33 +6216,33 @@ const docTemplate = `{
"application/json"
],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Update OAuth2 application.",
- "operationId": "update-oauth2-application",
+ "summary": "Create an AI model in an organization",
+ "operationId": "create-an-ai-model-in-an-organization",
"parameters": [
{
"type": "string",
- "description": "App ID",
- "name": "app",
+ "description": "Organization name or ID",
+ "name": "organization",
"in": "path",
"required": true
},
{
- "description": "Update an OAuth2 application.",
+ "description": "Model",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.PutOAuth2ProviderAppRequest"
+ "$ref": "#/definitions/codersdk.CreateChatModelRequest"
}
}
],
"responses": {
- "200": {
- "description": "OK",
+ "201": {
+ "description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderApp"
+ "$ref": "#/definitions/codersdk.ChatModel"
}
}
},
@@ -5731,50 +6250,35 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
- },
- "delete": {
- "tags": [
- "Enterprise"
- ],
- "summary": "Delete OAuth2 application.",
- "operationId": "delete-oauth2-application",
- "parameters": [
- {
- "type": "string",
- "description": "App ID",
- "name": "app",
- "in": "path",
- "required": true
- }
],
- "responses": {
- "204": {
- "description": "No Content"
- }
- },
- "security": [
- {
- "CoderSessionToken": []
- }
- ]
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/oauth2-provider/apps/{app}/secrets": {
+ "/api/v2/organizations/{organization}/chats/models/{model}": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Get OAuth2 application secrets.",
- "operationId": "get-oauth2-application-secrets",
+ "summary": "Get an AI model",
+ "operationId": "get-an-ai-model",
"parameters": [
{
"type": "string",
- "description": "App ID",
- "name": "app",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Model ID",
+ "name": "model",
"in": "path",
"required": true
}
@@ -5783,10 +6287,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderAppSecret"
- }
+ "$ref": "#/definitions/codersdk.ChatModel"
}
}
},
@@ -5794,94 +6295,136 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
- },
- "post": {
- "produces": [
- "application/json"
],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "delete": {
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Create OAuth2 application secret.",
- "operationId": "create-oauth2-application-secret",
+ "summary": "Delete an AI model",
+ "operationId": "delete-an-ai-model",
"parameters": [
{
"type": "string",
- "description": "App ID",
- "name": "app",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Model ID",
+ "name": "model",
"in": "path",
"required": true
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderAppSecretFull"
- }
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
- }
- },
- "/api/v2/oauth2-provider/apps/{app}/secrets/{secretID}": {
- "delete": {
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "patch": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Delete OAuth2 application secret.",
- "operationId": "delete-oauth2-application-secret",
+ "summary": "Update an AI model",
+ "operationId": "update-an-ai-model",
"parameters": [
{
"type": "string",
- "description": "App ID",
- "name": "app",
+ "description": "Organization name or ID",
+ "name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
- "description": "Secret ID",
- "name": "secretID",
+ "format": "uuid",
+ "description": "Model ID",
+ "name": "model",
"in": "path",
"required": true
+ },
+ {
+ "description": "Model updates",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateChatModelRequest"
+ }
}
],
"responses": {
- "204": {
- "description": "No Content"
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatModel"
+ }
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/oauth2-provider/settings": {
+ "/api/v2/organizations/{organization}/chats/models/{model}/acl": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "Chats"
+ ],
+ "summary": "Get an AI model ACL",
+ "operationId": "get-an-ai-model-acl",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Model ID",
+ "name": "model",
+ "in": "path",
+ "required": true
+ }
],
- "summary": "Get OAuth2 provider settings.",
- "operationId": "get-oauth2-provider-settings",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
+ "$ref": "#/definitions/codersdk.ChatModelACL"
}
}
},
@@ -5889,63 +6432,88 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
},
- "put": {
+ "patch": {
"consumes": [
"application/json"
],
- "produces": [
- "application/json"
- ],
"tags": [
- "Enterprise"
+ "Chats"
],
- "summary": "Update OAuth2 provider settings.",
- "operationId": "update-oauth2-provider-settings",
+ "summary": "Update an AI model ACL",
+ "operationId": "update-an-ai-model-acl",
"parameters": [
{
- "description": "OAuth2 provider settings request",
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Model ID",
+ "name": "model",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Sparse model ACL update",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
+ "$ref": "#/definitions/codersdk.UpdateChatModelACLRequest"
}
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/organizations": {
+ "/api/v2/organizations/{organization}/groups": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Organizations"
+ "Enterprise"
+ ],
+ "summary": "Get groups by organization",
+ "operationId": "get-groups-by-organization",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Organization ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ }
],
- "summary": "Get organizations",
- "operationId": "get-organizations",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/codersdk.Organization"
+ "$ref": "#/definitions/codersdk.Group"
}
}
}
@@ -5964,26 +6532,33 @@ const docTemplate = `{
"application/json"
],
"tags": [
- "Organizations"
+ "Enterprise"
],
- "summary": "Create organization",
- "operationId": "create-organization",
+ "summary": "Create group for organization",
+ "operationId": "create-group-for-organization",
"parameters": [
{
- "description": "Create organization request",
+ "description": "Create group request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateOrganizationRequest"
+ "$ref": "#/definitions/codersdk.CreateGroupRequest"
}
+ },
+ {
+ "type": "string",
+ "description": "Organization ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.Organization"
+ "$ref": "#/definitions/codersdk.Group"
}
}
},
@@ -5994,16 +6569,17 @@ const docTemplate = `{
]
}
},
- "/api/v2/organizations/{organization}": {
+ "/api/v2/organizations/{organization}/groups/ai/spend": {
"get": {
+ "description": "Returns AI spend limits and aggregate spend for the requested groups.\nA maximum of 100 group IDs may be requested per call, and requests with more are rejected, so callers are expected to batch across multiple requests.\nUnknown or unreadable group IDs are silently omitted.",
"produces": [
"application/json"
],
"tags": [
- "Organizations"
+ "Enterprise"
],
- "summary": "Get organization by ID",
- "operationId": "get-organization-by-id",
+ "summary": "Get organization groups AI spend",
+ "operationId": "get-organization-groups-ai-spend",
"parameters": [
{
"type": "string",
@@ -6012,13 +6588,20 @@ const docTemplate = `{
"name": "organization",
"in": "path",
"required": true
+ },
+ {
+ "type": "string",
+ "description": "Comma-separated list of group IDs (maximum 100)",
+ "name": "group_ids",
+ "in": "query",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Organization"
+ "$ref": "#/definitions/codersdk.OrganizationGroupsAISpend"
}
}
},
@@ -6027,30 +6610,40 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- },
- "delete": {
+ }
+ },
+ "/api/v2/organizations/{organization}/groups/{groupName}": {
+ "get": {
"produces": [
"application/json"
],
"tags": [
- "Organizations"
+ "Enterprise"
],
- "summary": "Delete organization",
- "operationId": "delete-organization",
+ "summary": "Get group by organization and group name",
+ "operationId": "get-group-by-organization-and-group-name",
"parameters": [
{
"type": "string",
- "description": "Organization ID or name",
+ "format": "uuid",
+ "description": "Organization ID",
"name": "organization",
"in": "path",
"required": true
+ },
+ {
+ "type": "string",
+ "description": "Group name",
+ "name": "groupName",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "$ref": "#/definitions/codersdk.Group"
}
}
},
@@ -6059,42 +6652,65 @@ const docTemplate = `{
"CoderSessionToken": []
}
]
- },
- "patch": {
- "consumes": [
- "application/json"
- ],
+ }
+ },
+ "/api/v2/organizations/{organization}/groups/{groupName}/members": {
+ "get": {
"produces": [
"application/json"
],
"tags": [
- "Organizations"
+ "Enterprise"
],
- "summary": "Update organization",
- "operationId": "update-organization",
+ "summary": "Get group members by organization and group name",
+ "operationId": "get-group-members-by-organization-and-group-name",
"parameters": [
{
"type": "string",
- "description": "Organization ID or name",
+ "format": "uuid",
+ "description": "Organization ID",
"name": "organization",
"in": "path",
"required": true
},
{
- "description": "Patch organization request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.UpdateOrganizationRequest"
- }
+ "type": "string",
+ "description": "Group name",
+ "name": "groupName",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Member search query",
+ "name": "q",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "After ID",
+ "name": "after_id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page limit",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page offset",
+ "name": "offset",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Organization"
+ "$ref": "#/definitions/codersdk.GroupMembersResponse"
}
}
},
@@ -6105,17 +6721,17 @@ const docTemplate = `{
]
}
},
- "/api/v2/organizations/{organization}/ai/spend/export": {
+ "/api/v2/organizations/{organization}/groups/{groupName}/members/ai/spend": {
"get": {
- "description": "Returns per-user, per-group, per-model, per-provider aggregated AI spend for the organization as CSV, built from raw AI Gateway token usage.\nThe optional period_start and period_end query parameters bound the period and are interpreted as UTC. They must be provided together and span at most 31 days. When both are omitted, the current UTC monthly period is used.\nAn explicit period_start must fall within the configured AI Gateway data retention window, since older token usage is purged. The default period is narrowed to that window instead, and every row echoes the applied bounds.\nRequires organization-level administrator permissions.",
+ "description": "Returns aggregate AI spend attributed to the group per requested user.\nA maximum of 100 user IDs may be requested per call, and requests with more are rejected, so callers are expected to batch across multiple requests.\nUser IDs that are not members of the group, or that the caller has no read access to, are silently omitted.",
"produces": [
- "text/csv"
+ "application/json"
],
"tags": [
"Enterprise"
],
- "summary": "Export organization AI spend as CSV",
- "operationId": "export-organization-ai-spend-as-csv",
+ "summary": "Get group members AI spend by organization",
+ "operationId": "get-group-members-ai-spend-by-organization",
"parameters": [
{
"type": "string",
@@ -6127,22 +6743,25 @@ const docTemplate = `{
},
{
"type": "string",
- "format": "date-time",
- "description": "Inclusive lower bound (RFC3339)",
- "name": "period_start",
- "in": "query"
+ "description": "Group name",
+ "name": "groupName",
+ "in": "path",
+ "required": true
},
{
"type": "string",
- "format": "date-time",
- "description": "Exclusive upper bound (RFC3339)",
- "name": "period_end",
- "in": "query"
+ "description": "Comma-separated list of user IDs (maximum 100)",
+ "name": "user_ids",
+ "in": "query",
+ "required": true
}
],
"responses": {
"200": {
- "description": "OK"
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.GroupMembersAISpend"
+ }
}
},
"security": [
@@ -6152,21 +6771,20 @@ const docTemplate = `{
]
}
},
- "/api/v2/organizations/{organization}/groups": {
+ "/api/v2/organizations/{organization}/mcp-servers": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "MCP"
],
- "summary": "Get groups by organization",
- "operationId": "get-groups-by-organization",
+ "summary": "List MCP server configs",
+ "operationId": "list-mcp-server-configs",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Organization ID",
+ "description": "Organization name or ID",
"name": "organization",
"in": "path",
"required": true
@@ -6178,7 +6796,7 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/codersdk.Group"
+ "$ref": "#/definitions/codersdk.MCPServerConfig"
}
}
}
@@ -6187,7 +6805,10 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
},
"post": {
"consumes": [
@@ -6197,33 +6818,33 @@ const docTemplate = `{
"application/json"
],
"tags": [
- "Enterprise"
+ "MCP"
],
- "summary": "Create group for organization",
- "operationId": "create-group-for-organization",
+ "summary": "Create MCP server config",
+ "operationId": "create-mcp-server-config",
"parameters": [
{
- "description": "Create group request",
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Create MCP server config request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateGroupRequest"
+ "$ref": "#/definitions/codersdk.CreateMCPServerConfigRequest"
}
- },
- {
- "type": "string",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.Group"
+ "$ref": "#/definitions/codersdk.MCPServerConfig"
}
}
},
@@ -6231,34 +6852,36 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/organizations/{organization}/groups/ai/spend": {
+ "/api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig}": {
"get": {
- "description": "Returns AI spend limits and aggregate spend for the requested groups.\nA maximum of 100 group IDs may be requested per call, and requests with more are rejected, so callers are expected to batch across multiple requests.\nUnknown or unreadable group IDs are silently omitted.",
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "MCP"
],
- "summary": "Get organization groups AI spend",
- "operationId": "get-organization-groups-ai-spend",
+ "summary": "Get MCP server config",
+ "operationId": "get-mcp-server-config",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Organization ID",
+ "description": "Organization name or ID",
"name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
- "description": "Comma-separated list of group IDs (maximum 100)",
- "name": "group_ids",
- "in": "query",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
+ "in": "path",
"required": true
}
],
@@ -6266,7 +6889,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.OrganizationGroupsAISpend"
+ "$ref": "#/definitions/codersdk.MCPServerConfig"
}
}
},
@@ -6274,41 +6897,91 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
- }
- },
- "/api/v2/organizations/{organization}/groups/{groupName}": {
- "get": {
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "delete": {
+ "tags": [
+ "MCP"
+ ],
+ "summary": "Delete MCP server config",
+ "operationId": "delete-mcp-server-config",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "patch": {
+ "consumes": [
+ "application/json"
+ ],
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "MCP"
],
- "summary": "Get group by organization and group name",
- "operationId": "get-group-by-organization-and-group-name",
+ "summary": "Update MCP server config",
+ "operationId": "update-mcp-server-config",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Organization ID",
+ "description": "Organization name or ID",
"name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
- "description": "Group name",
- "name": "groupName",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
"in": "path",
"required": true
+ },
+ {
+ "description": "Update MCP server config request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateMCPServerConfigRequest"
+ }
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Group"
+ "$ref": "#/definitions/codersdk.MCPServerConfig"
}
}
},
@@ -6316,124 +6989,143 @@ const docTemplate = `{
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/organizations/{organization}/groups/{groupName}/members": {
+ "/api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl": {
"get": {
"produces": [
"application/json"
],
"tags": [
- "Enterprise"
+ "MCP"
],
- "summary": "Get group members by organization and group name",
- "operationId": "get-group-members-by-organization-and-group-name",
+ "summary": "Get MCP server config ACL",
+ "operationId": "get-mcp-server-config-acl",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Organization ID",
+ "description": "Organization name or ID",
"name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
- "description": "Group name",
- "name": "groupName",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
"in": "path",
"required": true
- },
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.MCPServerConfigACL"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "patch": {
+ "consumes": [
+ "application/json"
+ ],
+ "tags": [
+ "MCP"
+ ],
+ "summary": "Update MCP server config ACL",
+ "operationId": "update-mcp-server-config-acl",
+ "parameters": [
{
"type": "string",
- "description": "Member search query",
- "name": "q",
- "in": "query"
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
},
{
"type": "string",
"format": "uuid",
- "description": "After ID",
- "name": "after_id",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Page limit",
- "name": "limit",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Page offset",
- "name": "offset",
- "in": "query"
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Update MCP server config ACL request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateMCPServerConfigACLRequest"
+ }
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.GroupMembersResponse"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/organizations/{organization}/groups/{groupName}/members/ai/spend": {
+ "/api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig}/oauth2/connect": {
"get": {
- "description": "Returns aggregate AI spend attributed to the group per requested user.\nA maximum of 100 user IDs may be requested per call, and requests with more are rejected, so callers are expected to batch across multiple requests.\nUser IDs that are not members of the group, or that the caller has no read access to, are silently omitted.",
- "produces": [
- "application/json"
- ],
"tags": [
- "Enterprise"
+ "MCP"
],
- "summary": "Get group members AI spend by organization",
- "operationId": "get-group-members-ai-spend-by-organization",
+ "summary": "Initiate MCP server OAuth2 connect",
+ "operationId": "initiate-mcp-server-oauth2-connect",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Organization ID",
+ "description": "Organization name or ID",
"name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
- "description": "Group name",
- "name": "groupName",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
"in": "path",
"required": true
- },
- {
- "type": "string",
- "description": "Comma-separated list of user IDs (maximum 100)",
- "name": "user_ids",
- "in": "query",
- "required": true
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.GroupMembersAISpend"
- }
+ "307": {
+ "description": "Temporary Redirect"
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
"/api/v2/organizations/{organization}/members": {
@@ -6766,6 +7458,112 @@ const docTemplate = `{
]
}
},
+ "/api/v2/organizations/{organization}/members/{user}/chats/model-overrides": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Chats"
+ ],
+ "summary": "Get organization member chat model overrides",
+ "operationId": "get-organization-member-chat-model-overrides",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "User name, ID, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.UserChatPersonalModelOverridesResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/v2/organizations/{organization}/members/{user}/chats/model-overrides/{context}": {
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
+ "tags": [
+ "Chats"
+ ],
+ "summary": "Update organization member chat model override",
+ "operationId": "update-organization-member-chat-model-override",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "User name, ID, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ },
+ {
+ "enum": [
+ "root",
+ "general",
+ "explore"
+ ],
+ "type": "string",
+ "description": "Override context",
+ "name": "context",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Personal model override",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateUserChatPersonalModelOverrideRequest"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
"/api/v2/organizations/{organization}/members/{user}/roles": {
"put": {
"consumes": [
@@ -10803,10 +11601,110 @@ const docTemplate = `{
"application/json"
],
"tags": [
- "Users"
+ "Users"
+ ],
+ "summary": "Get user by name",
+ "operationId": "get-user-by-name",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.User"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "delete": {
+ "tags": [
+ "Users"
+ ],
+ "summary": "Delete user",
+ "operationId": "delete-user",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "User ID, name, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/users/{user}/ai-provider-keys": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Chats"
+ ],
+ "summary": "List user AI provider key configurations",
+ "operationId": "list-user-ai-provider-key-configurations",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.UserAIProviderKeyConfig"
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/users/{user}/ai-provider-keys/{aiProvider}": {
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Chats"
],
- "summary": "Get user by name",
- "operationId": "get-user-by-name",
+ "summary": "Update user AI provider key",
+ "operationId": "update-user-ai-provider-key",
"parameters": [
{
"type": "string",
@@ -10814,13 +11712,30 @@ const docTemplate = `{
"name": "user",
"in": "path",
"required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "AI provider ID",
+ "name": "aiProvider",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Request body",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateUserAIProviderKeyRequest"
+ }
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.User"
+ "$ref": "#/definitions/codersdk.UserAIProviderKeyConfig"
}
}
},
@@ -10832,22 +11747,30 @@ const docTemplate = `{
},
"delete": {
"tags": [
- "Users"
+ "Chats"
],
- "summary": "Delete user",
- "operationId": "delete-user",
+ "summary": "Delete user AI provider key",
+ "operationId": "delete-user-ai-provider-key",
"parameters": [
{
"type": "string",
- "description": "User ID, name, or me",
+ "description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "AI provider ID",
+ "name": "aiProvider",
+ "in": "path",
+ "required": true
}
],
"responses": {
- "200": {
- "description": "OK"
+ "204": {
+ "description": "No Content"
}
},
"security": [
@@ -16138,6 +17061,12 @@ const docTemplate = `{
"ReinitializeReasonPrebuildClaimed"
]
},
+ "coderd.chatsByWorkspaceResponse": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
"coderd.cspViolation": {
"type": "object",
"properties": {
@@ -16929,6 +17858,33 @@ const docTemplate = `{
}
}
},
+ "codersdk.AIProviderSummary": {
+ "type": "object",
+ "properties": {
+ "deleted": {
+ "type": "boolean"
+ },
+ "display_name": {
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "icon": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "$ref": "#/definitions/codersdk.AIProviderType"
+ }
+ }
+ },
"codersdk.AIProviderType": {
"type": "string",
"enum": [
@@ -18353,6 +19309,14 @@ const docTemplate = `{
}
}
},
+ "codersdk.ChatAutoArchiveDaysResponse": {
+ "type": "object",
+ "properties": {
+ "auto_archive_days": {
+ "type": "integer"
+ }
+ }
+ },
"codersdk.ChatBusyBehavior": {
"type": "string",
"enum": [
@@ -18531,6 +19495,25 @@ const docTemplate = `{
}
}
},
+ "codersdk.ChatDebugLoggingAdminSettings": {
+ "type": "object",
+ "properties": {
+ "allow_users": {
+ "type": "boolean"
+ },
+ "forced_by_deployment": {
+ "type": "boolean"
+ }
+ }
+ },
+ "codersdk.ChatDebugRetentionDaysResponse": {
+ "type": "object",
+ "properties": {
+ "debug_retention_days": {
+ "type": "integer"
+ }
+ }
+ },
"codersdk.ChatDiffContents": {
"type": "object",
"properties": {
@@ -19755,6 +20738,14 @@ const docTemplate = `{
"ChatPersonalModelOverrideModeModel"
]
},
+ "codersdk.ChatPersonalModelOverridesAdminSettings": {
+ "type": "object",
+ "properties": {
+ "allow_users": {
+ "type": "boolean"
+ }
+ }
+ },
"codersdk.ChatPlanMode": {
"type": "string",
"enum": [
@@ -19764,6 +20755,14 @@ const docTemplate = `{
"ChatPlanModePlan"
]
},
+ "codersdk.ChatPlanModeInstructionsResponse": {
+ "type": "object",
+ "properties": {
+ "plan_mode_instructions": {
+ "type": "string"
+ }
+ }
+ },
"codersdk.ChatPrompt": {
"type": "object",
"properties": {
@@ -20000,6 +20999,20 @@ const docTemplate = `{
}
}
},
+ "codersdk.ChatSystemPromptResponse": {
+ "type": "object",
+ "properties": {
+ "default_system_prompt": {
+ "type": "string"
+ },
+ "include_default_system_prompt": {
+ "type": "boolean"
+ },
+ "system_prompt": {
+ "type": "string"
+ }
+ }
+ },
"codersdk.ChatUnsupportedProvider": {
"type": "object",
"properties": {
@@ -20087,6 +21100,15 @@ const docTemplate = `{
"ChatWatchEventKindContextDirty"
]
},
+ "codersdk.ChatWorkspaceTTLResponse": {
+ "type": "object",
+ "properties": {
+ "workspace_ttl_ms": {
+ "description": "WorkspaceTTLMillis is the workspace TTL in milliseconds.\nZero means disabled; the template's own autostop setting applies.",
+ "type": "integer"
+ }
+ }
+ },
"codersdk.ClusterConfig": {
"type": "object",
"properties": {
@@ -21087,6 +22109,14 @@ const docTemplate = `{
}
}
},
+ "codersdk.CreateUserAIProviderKeyRequest": {
+ "type": "object",
+ "properties": {
+ "api_key": {
+ "type": "string"
+ }
+ }
+ },
"codersdk.CreateUserRequestWithOrgs": {
"type": "object",
"required": [
@@ -26433,6 +27463,17 @@ const docTemplate = `{
}
}
},
+ "codersdk.SubmitToolResultsRequest": {
+ "type": "object",
+ "properties": {
+ "results": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.ToolResult"
+ }
+ }
+ }
+ },
"codersdk.SupportConfig": {
"type": "object",
"properties": {
@@ -27673,6 +28714,23 @@ const docTemplate = `{
}
}
},
+ "codersdk.ToolResult": {
+ "type": "object",
+ "properties": {
+ "is_error": {
+ "type": "boolean"
+ },
+ "output": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tool_call_id": {
+ "type": "string"
+ }
+ }
+ },
"codersdk.TraceConfig": {
"type": "object",
"properties": {
@@ -27783,6 +28841,30 @@ const docTemplate = `{
}
}
},
+ "codersdk.UpdateChatAutoArchiveDaysRequest": {
+ "type": "object",
+ "properties": {
+ "auto_archive_days": {
+ "type": "integer"
+ }
+ }
+ },
+ "codersdk.UpdateChatDebugLoggingAllowUsersRequest": {
+ "type": "object",
+ "properties": {
+ "allow_users": {
+ "type": "boolean"
+ }
+ }
+ },
+ "codersdk.UpdateChatDebugRetentionDaysRequest": {
+ "type": "object",
+ "properties": {
+ "debug_retention_days": {
+ "type": "integer"
+ }
+ }
+ },
"codersdk.UpdateChatModelACLRequest": {
"type": "object",
"properties": {
@@ -27841,6 +28923,22 @@ const docTemplate = `{
}
}
},
+ "codersdk.UpdateChatPersonalModelOverridesAdminSettingsRequest": {
+ "type": "object",
+ "properties": {
+ "allow_users": {
+ "type": "boolean"
+ }
+ }
+ },
+ "codersdk.UpdateChatPlanModeInstructionsRequest": {
+ "type": "object",
+ "properties": {
+ "plan_mode_instructions": {
+ "type": "string"
+ }
+ }
+ },
"codersdk.UpdateChatRequest": {
"type": "object",
"properties": {
@@ -27882,6 +28980,26 @@ const docTemplate = `{
}
}
},
+ "codersdk.UpdateChatSystemPromptRequest": {
+ "type": "object",
+ "properties": {
+ "include_default_system_prompt": {
+ "type": "boolean"
+ },
+ "system_prompt": {
+ "type": "string"
+ }
+ }
+ },
+ "codersdk.UpdateChatWorkspaceTTLRequest": {
+ "type": "object",
+ "properties": {
+ "workspace_ttl_ms": {
+ "description": "WorkspaceTTLMillis is the workspace TTL in milliseconds.\nZero means disabled; the template's own autostop setting applies.",
+ "type": "integer"
+ }
+ }
+ },
"codersdk.UpdateCheckResponse": {
"type": "object",
"properties": {
@@ -28225,6 +29343,24 @@ const docTemplate = `{
}
}
},
+ "codersdk.UpdateUserChatCompactionThresholdRequest": {
+ "type": "object",
+ "properties": {
+ "threshold_percent": {
+ "type": "integer",
+ "maximum": 100,
+ "minimum": 0
+ }
+ }
+ },
+ "codersdk.UpdateUserChatDebugLoggingRequest": {
+ "type": "object",
+ "properties": {
+ "debug_logging_enabled": {
+ "type": "boolean"
+ }
+ }
+ },
"codersdk.UpdateUserChatPersonalModelOverrideRequest": {
"type": "object",
"properties": {
@@ -28672,6 +29808,23 @@ const docTemplate = `{
}
}
},
+ "codersdk.UserAIProviderKeyConfig": {
+ "type": "object",
+ "properties": {
+ "byok_enabled": {
+ "type": "boolean"
+ },
+ "has_provider_api_key": {
+ "type": "boolean"
+ },
+ "has_user_api_key": {
+ "type": "boolean"
+ },
+ "provider": {
+ "$ref": "#/definitions/codersdk.AIProviderSummary"
+ }
+ }
+ },
"codersdk.UserAISpendStatus": {
"type": "object",
"properties": {
@@ -28792,6 +29945,51 @@ const docTemplate = `{
}
}
},
+ "codersdk.UserChatCompactionThreshold": {
+ "type": "object",
+ "properties": {
+ "model_config_id": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "threshold_percent": {
+ "type": "integer"
+ }
+ }
+ },
+ "codersdk.UserChatCompactionThresholds": {
+ "type": "object",
+ "properties": {
+ "thresholds": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.UserChatCompactionThreshold"
+ }
+ }
+ }
+ },
+ "codersdk.UserChatCustomPrompt": {
+ "type": "object",
+ "properties": {
+ "custom_prompt": {
+ "type": "string"
+ }
+ }
+ },
+ "codersdk.UserChatDebugLoggingSettings": {
+ "type": "object",
+ "properties": {
+ "debug_logging_enabled": {
+ "type": "boolean"
+ },
+ "forced_by_deployment": {
+ "type": "boolean"
+ },
+ "user_toggle_allowed": {
+ "type": "boolean"
+ }
+ }
+ },
"codersdk.UserChatPersonalModelOverridesResponse": {
"type": "object",
"properties": {
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index cd2a051eeb9..bf76c638592 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -127,34 +127,112 @@
}
}
},
- "/api/experimental/chats": {
+ "/api/experimental/chats/{chat}/stream/desktop": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
- "produces": ["application/json"],
+ "description": "Raw binary WebSocket stream of the chat workspace desktop.\nExperimental: this endpoint is subject to change.",
+ "produces": ["application/octet-stream"],
"tags": ["Chats"],
- "summary": "List chats",
- "operationId": "list-chats",
+ "summary": "Connect to chat workspace desktop via WebSockets",
+ "operationId": "connect-to-chat-workspace-desktop-via-websockets",
"parameters": [
{
"type": "string",
- "description": "Search query. Supports `title:\u003csubstring\u003e` (case-insensitive, quote multi-word values), `archived:bool`, `has_unread:bool`, `pr_status:\u003cdraft\\|open\\|merged\\|closed\u003e` as repeated or comma-separated values, `source:\u003ccreated_by_me\\|shared_with_me\u003e`, `diff_url:\u003curl\u003e` (quote values containing colons), `pr:\u003cnumber\u003e` (exact PR number match), `repo:\u003cowner/repo\u003e` (case-insensitive substring match against git remote origin or URL), `pr_title:\u003ctext\u003e` (case-insensitive PR title substring), `search:\u003ctext\u003e` (full-text search across chat titles, PR titles, PR numbers, and message bodies; message bodies match English word stems, e.g. `refactor` matches `refactoring`, and ignore English stopwords; titles and PR titles match whole words case-insensitively without stemming; quote multi-word values; cannot be combined with title, pr_title, or pr; a value that tokenizes to no searchable words, e.g. punctuation only, returns an empty list). Bare terms are not supported; use `title:\u003cvalue\u003e` or `search:\u003cvalue\u003e`.",
- "name": "q",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "101": {
+ "description": "Switching Protocols"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/experimental/mcp/servers/{mcpServer}/oauth2/callback": {
+ "get": {
+ "produces": ["text/html"],
+ "tags": ["MCP"],
+ "summary": "Handle MCP server OAuth2 callback",
+ "operationId": "handle-mcp-server-oauth2-callback",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpServer",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Authorization code issued by the provider. Required together with state on success.",
+ "name": "code",
"in": "query"
},
{
"type": "string",
- "description": "Filter by label as key:value. Repeat for multiple (AND logic).",
- "name": "label",
+ "description": "Opaque state issued by the connect endpoint. Required together with code on success.",
+ "name": "state",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Provider error code. Present instead of code when authorization fails.",
+ "name": "error",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Provider error description accompanying error.",
+ "name": "error_description",
"in": "query"
}
],
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/experimental/users/{user}/skills": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Users"],
+ "summary": "List user skills",
+ "operationId": "list-user-skills",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/codersdk.Chat"
+ "$ref": "#/definitions/codersdk.UserSkillMetadata"
}
}
}
@@ -163,23 +241,32 @@
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
},
"post": {
- "description": "Experimental: this endpoint is subject to change.",
"consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Create chat",
- "operationId": "create-chat",
+ "tags": ["Users"],
+ "summary": "Create a user skill",
+ "operationId": "create-a-user-skill",
"parameters": [
{
- "description": "Create chat request",
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Create user skill request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateChatRequest"
+ "$ref": "#/definitions/codersdk.CreateUserSkillRequest"
}
}
],
@@ -187,13 +274,7 @@
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.Chat"
- }
- },
- "413": {
- "description": "Request body exceeds 256 KiB",
- "schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "$ref": "#/definitions/codersdk.UserSkill"
}
}
},
@@ -201,20 +282,39 @@
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/experimental/chats/config/retention-days": {
+ "/api/experimental/users/{user}/skills/{skillName}": {
"get": {
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Get chat retention days",
- "operationId": "get-chat-retention-days",
+ "tags": ["Users"],
+ "summary": "Get a user skill by name",
+ "operationId": "get-a-user-skill-by-name",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Skill name",
+ "name": "skillName",
+ "in": "path",
+ "required": true
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatRetentionDaysResponse"
+ "$ref": "#/definitions/codersdk.UserSkill"
}
}
},
@@ -227,20 +327,24 @@
"skip": true
}
},
- "put": {
- "consumes": ["application/json"],
- "tags": ["Chats"],
- "summary": "Update chat retention days",
- "operationId": "update-chat-retention-days",
+ "delete": {
+ "tags": ["Users"],
+ "summary": "Delete a user skill",
+ "operationId": "delete-a-user-skill",
"parameters": [
{
- "description": "Request body",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.UpdateChatRetentionDaysRequest"
- }
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Skill name",
+ "name": "skillName",
+ "in": "path",
+ "required": true
}
],
"responses": {
@@ -256,47 +360,43 @@
"x-apidocgen": {
"skip": true
}
- }
- },
- "/api/experimental/chats/files": {
- "post": {
- "description": "Experimental: this endpoint is subject to change.",
- "consumes": [
- "image/png",
- "image/jpeg",
- "image/gif",
- "image/webp",
- "text/plain",
- "text/markdown",
- "text/csv",
- "application/json",
- "application/pdf"
- ],
+ },
+ "patch": {
+ "consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Upload chat file",
- "operationId": "upload-chat-file",
+ "tags": ["Users"],
+ "summary": "Update a user skill",
+ "operationId": "update-a-user-skill",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "query",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Skill name",
+ "name": "skillName",
+ "in": "path",
"required": true
+ },
+ {
+ "description": "Update user skill request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateUserSkillRequest"
+ }
}
],
"responses": {
- "201": {
- "description": "Created",
- "schema": {
- "$ref": "#/definitions/codersdk.UploadChatFileResponse"
- }
- },
- "413": {
- "description": "Request body exceeds 10 MiB",
+ "200": {
+ "description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "$ref": "#/definitions/codersdk.UserSkill"
}
}
},
@@ -304,114 +404,119 @@
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/experimental/chats/files/{file}": {
+ "/api/experimental/watch-all-workspacebuilds": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
- "produces": [
- "image/png",
- "image/jpeg",
- "image/gif",
- "image/webp",
- "text/plain",
- "text/markdown",
- "text/csv",
- "application/json",
- "application/pdf"
- ],
- "tags": ["Chats"],
- "summary": "Get chat file",
- "operationId": "get-chat-file",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "File ID",
- "name": "file",
- "in": "path",
- "required": true
- }
- ],
+ "produces": ["application/json"],
+ "tags": ["Workspaces"],
+ "summary": "Watch all workspace builds",
+ "operationId": "watch-all-workspace-builds",
"responses": {
- "200": {
- "description": "OK"
+ "101": {
+ "description": "Switching Protocols"
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/experimental/chats/files/{file}/download": {
+ "/api/v2/": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
- "produces": [
- "image/png",
- "image/jpeg",
- "image/gif",
- "image/webp",
- "text/plain",
- "text/markdown",
- "text/csv",
- "application/json",
- "application/pdf"
- ],
- "tags": ["Chats"],
- "summary": "Download chat file with signed token",
- "operationId": "download-chat-file",
+ "produces": ["application/json"],
+ "tags": ["General"],
+ "summary": "API root handler",
+ "operationId": "api-root-handler",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
+ }
+ }
+ }
+ }
+ },
+ "/api/v2/agent-firewall/sessions/{id}": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Enterprise"],
+ "summary": "Get agent firewall session by ID",
+ "operationId": "get-agent-firewall-session-by-id",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "File ID",
- "name": "file",
+ "description": "Agent firewall session ID",
+ "name": "id",
"in": "path",
"required": true
- },
- {
- "type": "string",
- "description": "Signed download token",
- "name": "token",
- "in": "query",
- "required": true
}
],
"responses": {
"200": {
- "description": "OK"
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.AgentFirewallSession"
+ }
}
},
- "x-apidocgen": {
- "skip": true
- }
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
}
},
- "/api/experimental/chats/files/{file}/download-url": {
- "post": {
- "description": "Experimental: this endpoint is subject to change.",
+ "/api/v2/agent-firewall/sessions/{id}/logs": {
+ "get": {
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Create chat file download URL",
- "operationId": "create-chat-file-download-url",
+ "tags": ["Enterprise"],
+ "summary": "Get agent firewall session logs",
+ "operationId": "get-agent-firewall-session-logs",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "File ID",
- "name": "file",
+ "description": "Agent firewall session ID",
+ "name": "id",
"in": "path",
"required": true
+ },
+ {
+ "type": "integer",
+ "description": "Inclusive lower bound on sequence number",
+ "name": "seq_after",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Exclusive upper bound on sequence number",
+ "name": "seq_before",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Maximum number of logs to return (default 100)",
+ "name": "limit",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatFileDownloadURLResponse"
+ "$ref": "#/definitions/codersdk.AgentFirewallSessionLogsResponse"
}
}
},
@@ -419,24 +524,24 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/chats/watch": {
+ "/api/v2/ai-gateway/clients": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
+ "description": "Alias: also available at /api/v2/aibridge/clients for backward compatibility.",
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Watch chat events for a user via WebSockets",
- "operationId": "watch-chat-events-for-a-user-via-websockets",
+ "tags": ["AI Gateway"],
+ "summary": "List AI Gateway clients",
+ "operationId": "list-ai-gateway-clients",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatWatchEvent"
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
}
},
@@ -447,28 +552,20 @@
]
}
},
- "/api/experimental/chats/{chat}": {
+ "/api/v2/ai-gateway/keys": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Get chat by ID",
- "operationId": "get-chat-by-id",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- }
- ],
+ "tags": ["Enterprise"],
+ "summary": "List AI Gateway keys",
+ "operationId": "list-ai-gateway-keys",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Chat"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.AIGatewayKey"
+ }
}
}
},
@@ -478,34 +575,29 @@
}
]
},
- "patch": {
- "description": "Experimental: this endpoint is subject to change.",
+ "post": {
"consumes": ["application/json"],
- "tags": ["Chats"],
- "summary": "Update chat",
- "operationId": "update-chat",
+ "produces": ["application/json"],
+ "tags": ["Enterprise"],
+ "summary": "Create AI Gateway key",
+ "operationId": "create-ai-gateway-key",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- },
- {
- "description": "Update chat request",
+ "description": "Create AI Gateway key request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateChatRequest"
+ "$ref": "#/definitions/codersdk.CreateAIGatewayKeyRequest"
}
}
],
"responses": {
- "204": {
- "description": "No Content"
+ "201": {
+ "description": "Created",
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateAIGatewayKeyResponse"
+ }
}
},
"security": [
@@ -515,137 +607,113 @@
]
}
},
- "/api/experimental/chats/{chat}/acl": {
- "get": {
- "description": "Experimental: this endpoint is subject to change.",
- "produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Get chat ACLs",
- "operationId": "get-chat-acls",
+ "/api/v2/ai-gateway/keys/{key}": {
+ "delete": {
+ "tags": ["Enterprise"],
+ "summary": "Delete AI Gateway key",
+ "operationId": "delete-ai-gateway-key",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "Chat ID",
- "name": "chat",
+ "description": "Key ID",
+ "name": "key",
"in": "path",
"required": true
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.ChatACL"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
- },
- "patch": {
- "description": "Experimental: this endpoint is subject to change.",
- "consumes": ["application/json"],
- "tags": ["Chats"],
- "summary": "Update chat ACL",
- "operationId": "update-chat-acl",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- },
- {
- "description": "Update chat ACL request",
- "name": "request",
- "in": "body",
- "required": true,
+ ]
+ }
+ },
+ "/api/v2/ai-gateway/models": {
+ "get": {
+ "description": "Alias: also available at /api/v2/aibridge/models for backward compatibility.",
+ "produces": ["application/json"],
+ "tags": ["AI Gateway"],
+ "summary": "List AI Gateway models",
+ "operationId": "list-ai-gateway-models",
+ "responses": {
+ "200": {
+ "description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.UpdateChatACL"
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
}
- ],
- "responses": {
- "204": {
- "description": "No Content"
- }
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/chats/{chat}/compact": {
- "post": {
- "description": "Experimental: this endpoint is subject to change.\nRequests a manual context compaction on an idle or errored\nchat, clearing any stored error. The compaction runs\nasynchronously through the chat worker and bypasses the\nautomatic usage threshold.",
- "produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Compact chat",
- "operationId": "compact-chat",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- }
- ],
+ "/api/v2/ai-gateway/serve": {
+ "get": {
+ "tags": ["Enterprise"],
+ "summary": "AI Gateway serve",
+ "operationId": "ai-gateway-serve",
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.Chat"
- }
+ "101": {
+ "description": "Switching Protocols"
}
},
"security": [
{
- "CoderSessionToken": []
+ "AIGatewayKey": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/chats/{chat}/context": {
- "put": {
- "description": "Experimental: this endpoint is subject to change.",
+ "/api/v2/ai-gateway/sessions": {
+ "get": {
+ "description": "Alias: also available at /api/v2/aibridge/sessions for backward compatibility.",
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Refresh chat context",
- "operationId": "refresh-chat-context",
+ "tags": ["AI Gateway"],
+ "summary": "List AI Gateway sessions",
+ "operationId": "list-ai-gateway-sessions",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
+ "description": "Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before.",
+ "name": "q",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page limit",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Cursor pagination after session ID (cannot be used with offset)",
+ "name": "after_session_id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Offset pagination (cannot be used with after_session_id)",
+ "name": "offset",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Chat"
+ "$ref": "#/definitions/codersdk.AIBridgeListSessionsResponse"
}
}
},
@@ -656,28 +724,45 @@
]
}
},
- "/api/experimental/chats/{chat}/cost": {
+ "/api/v2/ai-gateway/sessions/{session_id}": {
"get": {
- "description": "Experimental: this endpoint is subject to change.\n\nCost covers the whole chat tree: the root chat plus every\nsubagent chat beneath it. Requesting cost for a subagent chat\nreturns that same total.\n\nCost is derived from AI Gateway data, which is subject to its\nown retention period, 60 days by default, configured\nindependently of chat retention. Spend for requests older than\nthat period is no longer reported, so a chat whose requests\nhave all been purged reports zero cost.",
+ "description": "Alias: also available at /api/v2/aibridge/sessions/{session_id} for backward compatibility.",
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Get chat cost",
- "operationId": "get-chat-cost",
+ "tags": ["AI Gateway"],
+ "summary": "Get AI Gateway session threads",
+ "operationId": "get-ai-gateway-session-threads",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
+ "description": "Session ID (client_session_id or interception UUID)",
+ "name": "session_id",
"in": "path",
"required": true
+ },
+ {
+ "type": "string",
+ "description": "Thread pagination cursor (forward/older)",
+ "name": "after_id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Thread pagination cursor (backward/newer)",
+ "name": "before_id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Number of threads per page (default 50)",
+ "name": "limit",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatCost"
+ "$ref": "#/definitions/codersdk.AIBridgeSessionThreadsResponse"
}
}
},
@@ -688,28 +773,20 @@
]
}
},
- "/api/experimental/chats/{chat}/diff": {
+ "/api/v2/ai/providers": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Get chat diff contents",
- "operationId": "get-chat-diff-contents",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- }
- ],
+ "tags": ["AI Providers"],
+ "summary": "List AI providers",
+ "operationId": "list-ai-providers",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatDiffContents"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.AIProvider"
+ }
}
}
},
@@ -718,30 +795,29 @@
"CoderSessionToken": []
}
]
- }
- },
- "/api/experimental/chats/{chat}/interrupt": {
+ },
"post": {
- "description": "Experimental: this endpoint is subject to change.",
+ "consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Interrupt chat",
- "operationId": "interrupt-chat",
+ "tags": ["AI Providers"],
+ "summary": "Create an AI provider",
+ "operationId": "create-an-ai-provider",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
+ "description": "Create AI provider request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateAIProviderRequest"
+ }
}
],
"responses": {
- "200": {
- "description": "OK",
+ "201": {
+ "description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.Chat"
+ "$ref": "#/definitions/codersdk.AIProvider"
}
}
},
@@ -752,46 +828,26 @@
]
}
},
- "/api/experimental/chats/{chat}/messages": {
+ "/api/v2/ai/providers/{idOrName}": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "List chat messages",
- "operationId": "list-chat-messages",
+ "tags": ["AI Providers"],
+ "summary": "Get an AI provider",
+ "operationId": "get-an-ai-provider",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
+ "description": "Provider ID or name",
+ "name": "idOrName",
"in": "path",
"required": true
- },
- {
- "type": "integer",
- "description": "Return messages with id \u003c before_id",
- "name": "before_id",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Return messages with id \u003e after_id",
- "name": "after_id",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Page size, 1 to 200. Defaults to 50.",
- "name": "limit",
- "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatMessagesResponse"
+ "$ref": "#/definitions/codersdk.AIProvider"
}
}
},
@@ -801,38 +857,22 @@
}
]
},
- "post": {
- "description": "Experimental: this endpoint is subject to change.",
- "consumes": ["application/json"],
- "produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Send chat message",
- "operationId": "send-chat-message",
+ "delete": {
+ "tags": ["AI Providers"],
+ "summary": "Delete an AI provider",
+ "operationId": "delete-an-ai-provider",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
+ "description": "Provider ID or name",
+ "name": "idOrName",
"in": "path",
"required": true
- },
- {
- "description": "Create chat message request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.CreateChatMessageRequest"
- }
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.CreateChatMessageResponse"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
@@ -840,39 +880,28 @@
"CoderSessionToken": []
}
]
- }
- },
- "/api/experimental/chats/{chat}/messages/{message}": {
+ },
"patch": {
- "description": "Experimental: this endpoint is subject to change.",
"consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Edit chat message",
- "operationId": "edit-chat-message",
+ "tags": ["AI Providers"],
+ "summary": "Update an AI provider",
+ "operationId": "update-an-ai-provider",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- },
- {
- "type": "integer",
- "description": "Message ID",
- "name": "message",
+ "description": "Provider ID or name",
+ "name": "idOrName",
"in": "path",
"required": true
},
{
- "description": "Edit chat message request",
+ "description": "Update AI provider request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.EditChatMessageRequest"
+ "$ref": "#/definitions/codersdk.UpdateAIProviderRequest"
}
}
],
@@ -880,7 +909,7 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.EditChatMessageResponse"
+ "$ref": "#/definitions/codersdk.AIProvider"
}
}
},
@@ -891,34 +920,17 @@
]
}
},
- "/api/experimental/chats/{chat}/prompts": {
+ "/api/v2/appearance": {
"get": {
- "description": "Experimental: this endpoint is subject to change.\n\nReturns the user-authored prompts in a chat, newest first,\nwith each prompt's text parts concatenated in the order they\nwere authored. Used by the composer to power the up/down\narrow prompt-history cycle without paging through every\nmessage in the chat.",
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "List chat user prompts",
- "operationId": "list-chat-user-prompts",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- },
- {
- "type": "integer",
- "description": "Page size, 0 to 2000. 0 (the default) means the server-side default of 500.",
- "name": "limit",
- "in": "query"
- }
- ],
+ "tags": ["Enterprise"],
+ "summary": "Get appearance",
+ "operationId": "get-appearance",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatPromptsResponse"
+ "$ref": "#/definitions/codersdk.AppearanceConfig"
}
}
},
@@ -927,62 +939,29 @@
"CoderSessionToken": []
}
]
- }
- },
- "/api/experimental/chats/{chat}/reconcile-invalid": {
- "post": {
- "description": "Experimental: this endpoint is subject to change.",
+ },
+ "put": {
+ "consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Reconcile invalid chat state",
- "operationId": "reconcile-invalid-chat-state",
+ "tags": ["Enterprise"],
+ "summary": "Update appearance",
+ "operationId": "update-appearance",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- }
- ],
- "responses": {
- "200": {
- "description": "OK",
+ "description": "Update appearance request",
+ "name": "request",
+ "in": "body",
+ "required": true,
"schema": {
- "$ref": "#/definitions/codersdk.Chat"
+ "$ref": "#/definitions/codersdk.UpdateAppearanceConfig"
}
}
- },
- "security": [
- {
- "CoderSessionToken": []
- }
- ]
- }
- },
- "/api/experimental/chats/{chat}/stream": {
- "get": {
- "description": "Experimental: this endpoint is subject to change.",
- "produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Stream chat events via WebSockets",
- "operationId": "stream-chat-events-via-websockets",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- }
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatStreamEvent"
+ "$ref": "#/definitions/codersdk.UpdateAppearanceConfig"
}
}
},
@@ -993,26 +972,22 @@
]
}
},
- "/api/experimental/chats/{chat}/stream/desktop": {
+ "/api/v2/applications/auth-redirect": {
"get": {
- "description": "Raw binary WebSocket stream of the chat workspace desktop.\nExperimental: this endpoint is subject to change.",
- "produces": ["application/octet-stream"],
- "tags": ["Chats"],
- "summary": "Connect to chat workspace desktop via WebSockets",
- "operationId": "connect-to-chat-workspace-desktop-via-websockets",
+ "tags": ["Applications"],
+ "summary": "Redirect to URI with encrypted API key",
+ "operationId": "redirect-to-uri-with-encrypted-api-key",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
+ "description": "Redirect destination",
+ "name": "redirect_uri",
+ "in": "query"
}
],
"responses": {
- "101": {
- "description": "Switching Protocols"
+ "307": {
+ "description": "Temporary Redirect"
}
},
"security": [
@@ -1022,28 +997,18 @@
]
}
},
- "/api/experimental/chats/{chat}/stream/git": {
+ "/api/v2/applications/host": {
"get": {
- "description": "Experimental: this endpoint is subject to change.",
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Watch chat workspace git state via WebSockets",
- "operationId": "watch-chat-workspace-git-state-via-websockets",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
- }
- ],
+ "tags": ["Applications"],
+ "summary": "Get applications host",
+ "operationId": "get-applications-host",
+ "deprecated": true,
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.WorkspaceAgentGitServerMessage"
+ "$ref": "#/definitions/codersdk.AppHostResponse"
}
}
},
@@ -1054,28 +1019,29 @@
]
}
},
- "/api/experimental/chats/{chat}/stream/parts": {
- "get": {
- "description": "Experimental: this endpoint is subject to change.",
+ "/api/v2/applications/reconnecting-pty-signed-token": {
+ "post": {
+ "consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Stream chat parts via WebSockets",
- "operationId": "stream-chat-parts-via-websockets",
+ "tags": ["Enterprise"],
+ "summary": "Issue signed app token for reconnecting PTY",
+ "operationId": "issue-signed-app-token-for-reconnecting-pty",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
- "required": true
+ "description": "Issue reconnecting PTY signed token request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.IssueReconnectingPTYSignedTokenRequest"
+ }
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatStreamEvent"
+ "$ref": "#/definitions/codersdk.IssueReconnectingPTYSignedTokenResponse"
}
}
},
@@ -1089,28 +1055,38 @@
}
}
},
- "/api/experimental/chats/{chat}/title/propose": {
- "post": {
- "description": "Experimental: this endpoint is subject to change.",
+ "/api/v2/audit": {
+ "get": {
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Propose chat title",
- "operationId": "propose-chat-title",
+ "tags": ["Audit"],
+ "summary": "Get audit logs",
+ "operationId": "get-audit-logs",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Chat ID",
- "name": "chat",
- "in": "path",
+ "description": "Search query",
+ "name": "q",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page limit",
+ "name": "limit",
+ "in": "query",
"required": true
+ },
+ {
+ "type": "integer",
+ "description": "Page offset",
+ "name": "offset",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ProposeChatTitleResponse"
+ "$ref": "#/definitions/codersdk.AuditLogResponse"
}
}
},
@@ -1121,49 +1097,26 @@
]
}
},
- "/api/experimental/mcp/servers/{mcpServer}/oauth2/callback": {
- "get": {
- "produces": ["text/html"],
- "tags": ["MCP"],
- "summary": "Handle MCP server OAuth2 callback",
- "operationId": "handle-mcp-server-oauth2-callback",
+ "/api/v2/audit/testgenerate": {
+ "post": {
+ "consumes": ["application/json"],
+ "tags": ["Audit"],
+ "summary": "Generate fake audit log",
+ "operationId": "generate-fake-audit-log",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpServer",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "Authorization code issued by the provider. Required together with state on success.",
- "name": "code",
- "in": "query"
- },
- {
- "type": "string",
- "description": "Opaque state issued by the connect endpoint. Required together with code on success.",
- "name": "state",
- "in": "query"
- },
- {
- "type": "string",
- "description": "Provider error code. Present instead of code when authorization fails.",
- "name": "error",
- "in": "query"
- },
- {
- "type": "string",
- "description": "Provider error description accompanying error.",
- "name": "error_description",
- "in": "query"
+ "description": "Audit log request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateTestAuditLogRequest"
+ }
}
],
"responses": {
- "200": {
- "description": "OK"
+ "204": {
+ "description": "No Content"
}
},
"security": [
@@ -1176,60 +1129,45 @@
}
}
},
- "/api/experimental/mcp/servers/{mcpServer}/oauth2/disconnect": {
- "delete": {
+ "/api/v2/auth/scopes": {
+ "get": {
"produces": ["application/json"],
- "tags": ["MCP"],
- "summary": "Disconnect MCP server OAuth2 token",
- "operationId": "disconnect-mcp-server-oauth2-token",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpServer",
- "in": "path",
- "required": true
- }
- ],
+ "tags": ["Authorization"],
+ "summary": "List API key scopes",
+ "operationId": "list-api-key-scopes",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.MCPServerOAuth2DisconnectResponse"
+ "$ref": "#/definitions/codersdk.ExternalAPIKeyScopes"
}
}
- },
- "security": [
- {
- "CoderSessionToken": []
- }
- ],
- "x-apidocgen": {
- "skip": true
}
}
},
- "/api/experimental/organizations/{organization}/chats/model-overrides": {
- "get": {
+ "/api/v2/authcheck": {
+ "post": {
+ "consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "List organization chat model overrides",
- "operationId": "list-organization-chat-model-overrides",
+ "tags": ["Authorization"],
+ "summary": "Check authorization",
+ "operationId": "check-authorization",
"parameters": [
{
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
+ "description": "Authorization request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.AuthorizationRequest"
+ }
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatModelOverridesResponse"
+ "$ref": "#/definitions/codersdk.AuthorizationResponse"
}
}
},
@@ -1237,89 +1175,76 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/chats/model-overrides/{context}": {
- "put": {
- "consumes": ["application/json"],
+ "/api/v2/buildinfo": {
+ "get": {
"produces": ["application/json"],
- "tags": ["Chats"],
- "summary": "Update organization chat model override",
- "operationId": "update-organization-chat-model-override",
- "parameters": [
- {
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "enum": [
- "general",
- "explore",
- "title_generation",
- "compaction",
- "advisor"
- ],
- "type": "string",
- "description": "Override context",
- "name": "context",
- "in": "path",
- "required": true
- },
- {
- "description": "Model override",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.UpdateChatModelOverrideRequest"
- }
- }
- ],
+ "tags": ["General"],
+ "summary": "Build info",
+ "operationId": "build-info",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatModelOverrideResponse"
+ "$ref": "#/definitions/codersdk.BuildInfoResponse"
}
}
- },
- "security": [
- {
- "CoderSessionToken": []
- }
- ],
- "x-apidocgen": {
- "skip": true
}
}
},
- "/api/experimental/organizations/{organization}/chats/models": {
+ "/api/v2/chats": {
"get": {
"produces": ["application/json"],
"tags": ["Chats"],
- "summary": "List AI models and provider descriptors in an organization",
- "operationId": "list-ai-models-by-organization",
+ "summary": "List chats",
+ "operationId": "list-chats",
"parameters": [
{
"type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
+ "description": "Search query. Supports `title:\u003csubstring\u003e` (case-insensitive, quote multi-word values), `archived:bool`, `has_unread:bool`, `pr_status:\u003cdraft\\|open\\|merged\\|closed\u003e` as repeated or comma-separated values, `source:\u003ccreated_by_me\\|shared_with_me\u003e`, `diff_url:\u003curl\u003e` (quote values containing colons), `pr:\u003cnumber\u003e` (exact PR number match), `repo:\u003cowner/repo\u003e` (case-insensitive substring match against git remote origin or URL), `pr_title:\u003ctext\u003e` (case-insensitive PR title substring), `search:\u003ctext\u003e` (full-text search across chat titles, PR titles, PR numbers, and message bodies; message bodies match English word stems, e.g. `refactor` matches `refactoring`, and ignore English stopwords; titles and PR titles match whole words case-insensitively without stemming; quote multi-word values; cannot be combined with title, pr_title, or pr; a value that tokenizes to no searchable words, e.g. punctuation only, returns an empty list). Bare terms are not supported; use `title:\u003cvalue\u003e` or `search:\u003cvalue\u003e`.",
+ "name": "q",
+ "in": "query"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "collectionFormat": "multi",
+ "description": "Filter by label as key:value. Repeat for multiple (AND logic).",
+ "name": "label",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "After ID",
+ "name": "after_id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page limit",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page offset",
+ "name": "offset",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.OrganizationChatModelsResponse"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.Chat"
+ }
}
}
},
@@ -1327,32 +1252,22 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
"post": {
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Chats"],
- "summary": "Create an AI model in an organization",
- "operationId": "create-ai-model",
+ "summary": "Create chat",
+ "operationId": "create-chat",
"parameters": [
{
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "description": "Model",
+ "description": "Create chat request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateChatModelRequest"
+ "$ref": "#/definitions/codersdk.CreateChatRequest"
}
}
],
@@ -1360,7 +1275,13 @@
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.ChatModel"
+ "$ref": "#/definitions/codersdk.Chat"
+ }
+ },
+ "413": {
+ "description": "Request body exceeds 256 KiB",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
}
}
},
@@ -1368,39 +1289,28 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/chats/models/{model}": {
+ "/api/v2/chats/by-workspace": {
"get": {
"produces": ["application/json"],
"tags": ["Chats"],
- "summary": "Get an AI model",
- "operationId": "get-ai-model",
+ "summary": "List chats by workspace",
+ "operationId": "list-chats-by-workspace",
"parameters": [
{
"type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "Model ID",
- "name": "model",
- "in": "path",
- "required": true
+ "description": "Comma-separated workspace IDs",
+ "name": "workspace_ids",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatModel"
+ "$ref": "#/definitions/coderd.chatsByWorkspaceResponse"
}
}
},
@@ -1408,122 +1318,68 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
- },
- "delete": {
+ ]
+ }
+ },
+ "/api/v2/chats/config/auto-archive-days": {
+ "get": {
+ "produces": ["application/json"],
"tags": ["Chats"],
- "summary": "Delete an AI model",
- "operationId": "delete-ai-model",
- "parameters": [
- {
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "Model ID",
- "name": "model",
- "in": "path",
- "required": true
- }
- ],
+ "summary": "Get chat auto archive days",
+ "operationId": "get-chat-auto-archive-days",
"responses": {
- "204": {
- "description": "No Content"
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatAutoArchiveDaysResponse"
+ }
}
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
- "patch": {
+ "put": {
"consumes": ["application/json"],
- "produces": ["application/json"],
"tags": ["Chats"],
- "summary": "Update an AI model",
- "operationId": "update-ai-model",
+ "summary": "Update chat auto archive days",
+ "operationId": "update-chat-auto-archive-days",
"parameters": [
{
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "Model ID",
- "name": "model",
- "in": "path",
- "required": true
- },
- {
- "description": "Model updates",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateChatModelRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatAutoArchiveDaysRequest"
}
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.ChatModel"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/chats/models/{model}/acl": {
+ "/api/v2/chats/config/debug-logging": {
"get": {
"produces": ["application/json"],
"tags": ["Chats"],
- "summary": "Get an AI model ACL",
- "operationId": "get-ai-model-acl",
- "parameters": [
- {
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "Model ID",
- "name": "model",
- "in": "path",
- "required": true
- }
- ],
+ "summary": "Get chat debug logging setting",
+ "operationId": "get-chat-debug-logging-setting",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ChatModelACL"
+ "$ref": "#/definitions/codersdk.ChatDebugLoggingAdminSettings"
}
}
},
@@ -1531,39 +1387,21 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
- "patch": {
+ "put": {
"consumes": ["application/json"],
"tags": ["Chats"],
- "summary": "Update an AI model ACL",
- "operationId": "update-ai-model-acl",
+ "summary": "Update chat debug logging setting",
+ "operationId": "update-chat-debug-logging-setting",
"parameters": [
{
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "Model ID",
- "name": "model",
- "in": "path",
- "required": true
- },
- {
- "description": "Sparse model ACL update",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateChatModelACLRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatDebugLoggingAllowUsersRequest"
}
}
],
@@ -1576,36 +1414,20 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/mcp-servers": {
+ "/api/v2/chats/config/debug-retention-days": {
"get": {
"produces": ["application/json"],
- "tags": ["MCP"],
- "summary": "List MCP server configs",
- "operationId": "list-mcp-server-configs",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- }
- ],
+ "tags": ["Chats"],
+ "summary": "Get chat debug retention days",
+ "operationId": "get-chat-debug-retention-days",
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.MCPServerConfig"
- }
+ "$ref": "#/definitions/codersdk.ChatDebugRetentionDaysResponse"
}
}
},
@@ -1613,83 +1435,47 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
- "post": {
+ "put": {
"consumes": ["application/json"],
- "produces": ["application/json"],
- "tags": ["MCP"],
- "summary": "Create MCP server config",
- "operationId": "create-mcp-server-config",
+ "tags": ["Chats"],
+ "summary": "Update chat debug retention days",
+ "operationId": "update-chat-debug-retention-days",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "description": "Create MCP server config request",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateMCPServerConfigRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatDebugRetentionDaysRequest"
}
}
],
"responses": {
- "201": {
- "description": "Created",
- "schema": {
- "$ref": "#/definitions/codersdk.MCPServerConfig"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}": {
+ "/api/v2/chats/config/personal-model-overrides": {
"get": {
"produces": ["application/json"],
- "tags": ["MCP"],
- "summary": "Get MCP server config",
- "operationId": "get-mcp-server-config",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
- "in": "path",
- "required": true
- }
- ],
+ "tags": ["Chats"],
+ "summary": "Get chat personal model override settings",
+ "operationId": "get-chat-personal-model-override-settings",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.MCPServerConfig"
+ "$ref": "#/definitions/codersdk.ChatPersonalModelOverridesAdminSettings"
}
}
},
@@ -1697,31 +1483,22 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
- "delete": {
- "tags": ["MCP"],
- "summary": "Delete MCP server config",
- "operationId": "delete-mcp-server-config",
+ "put": {
+ "consumes": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Update chat personal model override settings",
+ "operationId": "update-chat-personal-model-override-settings",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
- "in": "path",
- "required": true
+ "description": "Request body",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateChatPersonalModelOverridesAdminSettingsRequest"
+ }
}
],
"responses": {
@@ -1733,91 +1510,68 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
+ }
+ },
+ "/api/v2/chats/config/plan-mode-instructions": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Get chat plan mode instructions",
+ "operationId": "get-chat-plan-mode-instructions",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatPlanModeInstructionsResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
},
- "patch": {
+ "put": {
"consumes": ["application/json"],
- "produces": ["application/json"],
- "tags": ["MCP"],
- "summary": "Update MCP server config",
- "operationId": "update-mcp-server-config",
+ "tags": ["Chats"],
+ "summary": "Update chat plan mode instructions",
+ "operationId": "update-chat-plan-mode-instructions",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
- "in": "path",
- "required": true
- },
- {
- "description": "Update MCP server config request",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateMCPServerConfigRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatPlanModeInstructionsRequest"
}
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.MCPServerConfig"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl": {
+ "/api/v2/chats/config/retention-days": {
"get": {
"produces": ["application/json"],
- "tags": ["MCP"],
- "summary": "Get MCP server config ACL",
- "operationId": "get-mcp-server-config-acl",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
- "in": "path",
- "required": true
- }
- ],
+ "tags": ["Chats"],
+ "summary": "Get chat retention days",
+ "operationId": "get-chat-retention-days",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.MCPServerConfigACL"
+ "$ref": "#/definitions/codersdk.ChatRetentionDaysResponse"
}
}
},
@@ -1830,35 +1584,19 @@
"skip": true
}
},
- "patch": {
+ "put": {
"consumes": ["application/json"],
- "tags": ["MCP"],
- "summary": "Update MCP server config ACL",
- "operationId": "update-mcp-server-config-acl",
+ "tags": ["Chats"],
+ "summary": "Update chat retention days",
+ "operationId": "update-chat-retention-days",
"parameters": [
{
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
- "in": "path",
- "required": true
- },
- {
- "description": "Update MCP server config ACL request",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateMCPServerConfigACLRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatRetentionDaysRequest"
}
}
],
@@ -1877,71 +1615,17 @@
}
}
},
- "/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/oauth2/connect": {
- "get": {
- "tags": ["MCP"],
- "summary": "Initiate MCP server OAuth2 connect",
- "operationId": "initiate-mcp-server-oauth2-connect",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "format": "uuid",
- "description": "MCP server config ID",
- "name": "mcpserverconfig",
- "in": "path",
- "required": true
- }
- ],
- "responses": {
- "307": {
- "description": "Temporary Redirect"
- }
- },
- "security": [
- {
- "CoderSessionToken": []
- }
- ],
- "x-apidocgen": {
- "skip": true
- }
- }
- },
- "/api/experimental/organizations/{organization}/members/{user}/chats/model-overrides": {
+ "/api/v2/chats/config/system-prompt": {
"get": {
"produces": ["application/json"],
"tags": ["Chats"],
- "summary": "Get organization member chat model overrides",
- "operationId": "get-organization-member-chat-model-overrides",
- "parameters": [
- {
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "User name, ID, or me",
- "name": "user",
- "in": "path",
- "required": true
- }
- ],
+ "summary": "Get chat system prompt",
+ "operationId": "get-chat-system-prompt",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.UserChatPersonalModelOverridesResponse"
+ "$ref": "#/definitions/codersdk.ChatSystemPromptResponse"
}
}
},
@@ -1949,48 +1633,21 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
- }
- },
- "/api/experimental/organizations/{organization}/members/{user}/chats/model-overrides/{context}": {
+ ]
+ },
"put": {
"consumes": ["application/json"],
"tags": ["Chats"],
- "summary": "Update organization member chat model override",
- "operationId": "update-organization-member-chat-model-override",
+ "summary": "Update chat system prompt",
+ "operationId": "update-chat-system-prompt",
"parameters": [
{
- "type": "string",
- "description": "Organization name or ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "User name, ID, or me",
- "name": "user",
- "in": "path",
- "required": true
- },
- {
- "enum": ["root", "general", "explore"],
- "type": "string",
- "description": "Override context",
- "name": "context",
- "in": "path",
- "required": true
- },
- {
- "description": "Personal model override",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateUserChatPersonalModelOverrideRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatSystemPromptRequest"
}
}
],
@@ -2003,35 +1660,20 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/users/{user}/skills": {
+ "/api/v2/chats/config/user-compaction-thresholds": {
"get": {
"produces": ["application/json"],
- "tags": ["Users"],
- "summary": "List user skills",
- "operationId": "list-user-skills",
- "parameters": [
- {
- "type": "string",
- "description": "User ID, username, or me",
- "name": "user",
- "in": "path",
- "required": true
- }
- ],
+ "tags": ["Chats"],
+ "summary": "Get user chat compaction thresholds",
+ "operationId": "get-user-chat-compaction-thresholds",
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.UserSkillMetadata"
- }
+ "$ref": "#/definitions/codersdk.UserChatCompactionThresholds"
}
}
},
@@ -2043,36 +1685,39 @@
"x-apidocgen": {
"skip": true
}
- },
- "post": {
+ }
+ },
+ "/api/v2/chats/config/user-compaction-thresholds/{modelConfig}": {
+ "put": {
"consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Users"],
- "summary": "Create a user skill",
- "operationId": "create-a-user-skill",
+ "tags": ["Chats"],
+ "summary": "Update user chat compaction threshold",
+ "operationId": "update-user-chat-compaction-threshold",
"parameters": [
{
"type": "string",
- "description": "User ID, username, or me",
- "name": "user",
+ "format": "uuid",
+ "description": "Model config ID",
+ "name": "modelConfig",
"in": "path",
"required": true
},
{
- "description": "Create user skill request",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateUserSkillRequest"
+ "$ref": "#/definitions/codersdk.UpdateUserChatCompactionThresholdRequest"
}
}
],
"responses": {
- "201": {
- "description": "Created",
+ "200": {
+ "description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.UserSkill"
+ "$ref": "#/definitions/codersdk.UserChatCompactionThreshold"
}
}
},
@@ -2084,35 +1729,47 @@
"x-apidocgen": {
"skip": true
}
- }
- },
- "/api/experimental/users/{user}/skills/{skillName}": {
- "get": {
- "produces": ["application/json"],
- "tags": ["Users"],
- "summary": "Get a user skill by name",
- "operationId": "get-a-user-skill-by-name",
+ },
+ "delete": {
+ "tags": ["Chats"],
+ "summary": "Delete user chat compaction threshold",
+ "operationId": "delete-user-chat-compaction-threshold",
"parameters": [
{
"type": "string",
- "description": "User ID, username, or me",
- "name": "user",
+ "format": "uuid",
+ "description": "Model config ID",
+ "name": "modelConfig",
"in": "path",
"required": true
- },
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
{
- "type": "string",
- "description": "Skill name",
- "name": "skillName",
- "in": "path",
- "required": true
+ "CoderSessionToken": []
}
],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/v2/chats/config/user-debug-logging": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Get user chat debug logging setting",
+ "operationId": "get-user-chat-debug-logging-setting",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.UserSkill"
+ "$ref": "#/definitions/codersdk.UserChatDebugLoggingSettings"
}
}
},
@@ -2120,29 +1777,22 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
},
- "delete": {
- "tags": ["Users"],
- "summary": "Delete a user skill",
- "operationId": "delete-a-user-skill",
+ "put": {
+ "consumes": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Update user chat debug logging setting",
+ "operationId": "update-user-chat-debug-logging-setting",
"parameters": [
{
- "type": "string",
- "description": "User ID, username, or me",
- "name": "user",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "Skill name",
- "name": "skillName",
- "in": "path",
- "required": true
+ "description": "Request body",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateUserChatDebugLoggingRequest"
+ }
}
],
"responses": {
@@ -2154,39 +1804,43 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
+ }
+ },
+ "/api/v2/chats/config/user-prompt": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Get user chat custom prompt",
+ "operationId": "get-user-chat-custom-prompt",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.UserChatCustomPrompt"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
},
- "patch": {
+ "put": {
"consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Users"],
- "summary": "Update a user skill",
- "operationId": "update-a-user-skill",
+ "tags": ["Chats"],
+ "summary": "Update user chat custom prompt",
+ "operationId": "update-user-chat-custom-prompt",
"parameters": [
{
- "type": "string",
- "description": "User ID, username, or me",
- "name": "user",
- "in": "path",
- "required": true
- },
- {
- "type": "string",
- "description": "Skill name",
- "name": "skillName",
- "in": "path",
- "required": true
- },
- {
- "description": "Update user skill request",
+ "description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateUserSkillRequest"
+ "$ref": "#/definitions/codersdk.UserChatCustomPrompt"
}
}
],
@@ -2194,7 +1848,7 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.UserSkill"
+ "$ref": "#/definitions/codersdk.UserChatCustomPrompt"
}
}
},
@@ -2202,71 +1856,154 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/experimental/watch-all-workspacebuilds": {
+ "/api/v2/chats/config/workspace-ttl": {
"get": {
"produces": ["application/json"],
- "tags": ["Workspaces"],
- "summary": "Watch all workspace builds",
- "operationId": "watch-all-workspace-builds",
+ "tags": ["Chats"],
+ "summary": "Get chat workspace time to live",
+ "operationId": "get-chat-workspace-time-to-live",
"responses": {
- "101": {
- "description": "Switching Protocols"
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatWorkspaceTTLResponse"
+ }
}
},
"security": [
{
"CoderSessionToken": []
}
+ ]
+ },
+ "put": {
+ "consumes": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Update chat workspace time to live",
+ "operationId": "update-chat-workspace-time-to-live",
+ "parameters": [
+ {
+ "description": "Request body",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateChatWorkspaceTTLRequest"
+ }
+ }
],
- "x-apidocgen": {
- "skip": true
- }
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
}
},
- "/api/v2/": {
- "get": {
+ "/api/v2/chats/files": {
+ "post": {
+ "consumes": [
+ "image/png",
+ "image/jpeg",
+ "image/gif",
+ "image/webp",
+ "text/plain",
+ "text/markdown",
+ "text/csv",
+ "application/json",
+ "application/pdf"
+ ],
"produces": ["application/json"],
- "tags": ["General"],
- "summary": "API root handler",
- "operationId": "api-root-handler",
+ "tags": ["Chats"],
+ "summary": "Upload chat file",
+ "operationId": "upload-chat-file",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Organization ID",
+ "name": "organization",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "example": "attachment; filename=\"image.png\"",
+ "description": "Attachment disposition carrying the file name",
+ "name": "Content-Disposition",
+ "in": "header",
+ "required": true
+ },
+ {
+ "description": "Raw file binary data",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
"responses": {
- "200": {
- "description": "OK",
+ "201": {
+ "description": "Created",
+ "schema": {
+ "$ref": "#/definitions/codersdk.UploadChatFileResponse"
+ }
+ },
+ "413": {
+ "description": "Request body exceeds 10 MiB",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
}
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "rawBodyFile": "image.png"
}
}
},
- "/api/v2/agent-firewall/sessions/{id}": {
+ "/api/v2/chats/files/{file}": {
"get": {
- "produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Get agent firewall session by ID",
- "operationId": "get-agent-firewall-session-by-id",
+ "produces": [
+ "image/png",
+ "image/jpeg",
+ "image/gif",
+ "image/webp",
+ "text/plain",
+ "text/markdown",
+ "text/csv",
+ "application/json",
+ "application/pdf"
+ ],
+ "tags": ["Chats"],
+ "summary": "Get chat file",
+ "operationId": "get-chat-file",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "Agent firewall session ID",
- "name": "id",
+ "description": "File ID",
+ "name": "file",
"in": "path",
"required": true
}
],
"responses": {
"200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.AgentFirewallSession"
- }
+ "description": "OK"
}
},
"security": [
@@ -2276,45 +2013,70 @@
]
}
},
- "/api/v2/agent-firewall/sessions/{id}/logs": {
+ "/api/v2/chats/files/{file}/download": {
"get": {
- "produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Get agent firewall session logs",
- "operationId": "get-agent-firewall-session-logs",
+ "produces": [
+ "image/png",
+ "image/jpeg",
+ "image/gif",
+ "image/webp",
+ "text/plain",
+ "text/markdown",
+ "text/csv",
+ "application/json",
+ "application/pdf"
+ ],
+ "tags": ["Chats"],
+ "summary": "Download chat file with signed token",
+ "operationId": "download-chat-file-with-signed-token",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "Agent firewall session ID",
- "name": "id",
+ "description": "File ID",
+ "name": "file",
"in": "path",
"required": true
},
{
- "type": "integer",
- "description": "Inclusive lower bound on sequence number",
- "name": "seq_after",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Exclusive upper bound on sequence number",
- "name": "seq_before",
- "in": "query"
- },
+ "type": "string",
+ "description": "Signed download token",
+ "name": "token",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/v2/chats/files/{file}/download-url": {
+ "post": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Create chat file download URL",
+ "operationId": "create-chat-file-download-url",
+ "parameters": [
{
- "type": "integer",
- "description": "Maximum number of logs to return (default 100)",
- "name": "limit",
- "in": "query"
+ "type": "string",
+ "format": "uuid",
+ "description": "File ID",
+ "name": "file",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AgentFirewallSessionLogsResponse"
+ "$ref": "#/definitions/codersdk.ChatFileDownloadURLResponse"
}
}
},
@@ -2322,24 +2084,23 @@
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/ai-gateway/clients": {
+ "/api/v2/chats/watch": {
"get": {
- "description": "Alias: also available at /api/v2/aibridge/clients for backward compatibility.",
"produces": ["application/json"],
- "tags": ["AI Gateway"],
- "summary": "List AI Gateway clients",
- "operationId": "list-ai-gateway-clients",
+ "tags": ["Chats"],
+ "summary": "Watch chat events for a user via WebSockets",
+ "operationId": "watch-chat-events-for-a-user-via-websockets",
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "$ref": "#/definitions/codersdk.ChatWatchEvent"
}
}
},
@@ -2350,20 +2111,27 @@
]
}
},
- "/api/v2/ai-gateway/keys": {
+ "/api/v2/chats/{chat}": {
"get": {
"produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "List AI Gateway keys",
- "operationId": "list-ai-gateway-keys",
+ "tags": ["Chats"],
+ "summary": "Get chat by ID",
+ "operationId": "get-chat-by-id",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.AIGatewayKey"
- }
+ "$ref": "#/definitions/codersdk.Chat"
}
}
},
@@ -2373,29 +2141,33 @@
}
]
},
- "post": {
+ "patch": {
"consumes": ["application/json"],
- "produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Create AI Gateway key",
- "operationId": "create-ai-gateway-key",
+ "tags": ["Chats"],
+ "summary": "Update chat",
+ "operationId": "update-chat",
"parameters": [
{
- "description": "Create AI Gateway key request",
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Update chat request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.CreateAIGatewayKeyRequest"
+ "$ref": "#/definitions/codersdk.UpdateChatRequest"
}
}
],
"responses": {
- "201": {
- "description": "Created",
- "schema": {
- "$ref": "#/definitions/codersdk.CreateAIGatewayKeyResponse"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
@@ -2405,48 +2177,27 @@
]
}
},
- "/api/v2/ai-gateway/keys/{key}": {
- "delete": {
- "tags": ["Enterprise"],
- "summary": "Delete AI Gateway key",
- "operationId": "delete-ai-gateway-key",
+ "/api/v2/chats/{chat}/acl": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Get chat ACLs",
+ "operationId": "get-chat-acls",
"parameters": [
{
"type": "string",
"format": "uuid",
- "description": "Key ID",
- "name": "key",
+ "description": "Chat ID",
+ "name": "chat",
"in": "path",
"required": true
}
],
- "responses": {
- "204": {
- "description": "No Content"
- }
- },
- "security": [
- {
- "CoderSessionToken": []
- }
- ]
- }
- },
- "/api/v2/ai-gateway/models": {
- "get": {
- "description": "Alias: also available at /api/v2/aibridge/models for backward compatibility.",
- "produces": ["application/json"],
- "tags": ["AI Gateway"],
- "summary": "List AI Gateway models",
- "operationId": "list-ai-gateway-models",
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "$ref": "#/definitions/codersdk.ChatACL"
}
}
},
@@ -2454,64 +2205,72 @@
{
"CoderSessionToken": []
}
- ]
- }
- },
- "/api/v2/ai-gateway/serve": {
- "get": {
- "tags": ["Enterprise"],
- "summary": "AI Gateway serve",
- "operationId": "ai-gateway-serve",
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "patch": {
+ "consumes": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Update chat ACL",
+ "operationId": "update-chat-acl",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Update chat ACL request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateChatACL"
+ }
+ }
+ ],
"responses": {
- "101": {
- "description": "Switching Protocols"
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
- "AIGatewayKey": []
+ "CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/ai-gateway/sessions": {
- "get": {
- "description": "Alias: also available at /api/v2/aibridge/sessions for backward compatibility.",
+ "/api/v2/chats/{chat}/compact": {
+ "post": {
+ "description": "Requests a manual context compaction on an idle or errored\nchat, clearing any stored error. The compaction runs\nasynchronously through the chat worker and bypasses the\nautomatic usage threshold.",
"produces": ["application/json"],
- "tags": ["AI Gateway"],
- "summary": "List AI Gateway sessions",
- "operationId": "list-ai-gateway-sessions",
+ "tags": ["Chats"],
+ "summary": "Compact chat",
+ "operationId": "compact-chat",
"parameters": [
{
"type": "string",
- "description": "Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before.",
- "name": "q",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Page limit",
- "name": "limit",
- "in": "query"
- },
- {
- "type": "string",
- "description": "Cursor pagination after session ID (cannot be used with offset)",
- "name": "after_session_id",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Offset pagination (cannot be used with after_session_id)",
- "name": "offset",
- "in": "query"
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AIBridgeListSessionsResponse"
+ "$ref": "#/definitions/codersdk.Chat"
}
}
},
@@ -2519,48 +2278,33 @@
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/ai-gateway/sessions/{session_id}": {
- "get": {
- "description": "Alias: also available at /api/v2/aibridge/sessions/{session_id} for backward compatibility.",
+ "/api/v2/chats/{chat}/context": {
+ "put": {
"produces": ["application/json"],
- "tags": ["AI Gateway"],
- "summary": "Get AI Gateway session threads",
- "operationId": "get-ai-gateway-session-threads",
+ "tags": ["Chats"],
+ "summary": "Refresh chat context",
+ "operationId": "refresh-chat-context",
"parameters": [
{
"type": "string",
- "description": "Session ID (client_session_id or interception UUID)",
- "name": "session_id",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
"in": "path",
"required": true
- },
- {
- "type": "string",
- "description": "Thread pagination cursor (forward/older)",
- "name": "after_id",
- "in": "query"
- },
- {
- "type": "string",
- "description": "Thread pagination cursor (backward/newer)",
- "name": "before_id",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Number of threads per page (default 50)",
- "name": "limit",
- "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AIBridgeSessionThreadsResponse"
+ "$ref": "#/definitions/codersdk.Chat"
}
}
},
@@ -2571,20 +2315,28 @@
]
}
},
- "/api/v2/ai/providers": {
+ "/api/v2/chats/{chat}/cost": {
"get": {
+ "description": "Cost covers the whole chat tree: the root chat plus every\nsubagent chat beneath it. Requesting cost for a subagent chat\nreturns that same total.\n\nCost is derived from AI Gateway data, which is subject to its\nown retention period, 60 days by default, configured\nindependently of chat retention. Spend for requests older than\nthat period is no longer reported, so a chat whose requests\nhave all been purged reports zero cost.",
"produces": ["application/json"],
- "tags": ["AI Providers"],
- "summary": "List AI providers",
- "operationId": "list-ai-providers",
+ "tags": ["Chats"],
+ "summary": "Get chat cost",
+ "operationId": "get-chat-cost",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.AIProvider"
- }
+ "$ref": "#/definitions/codersdk.ChatCost"
}
}
},
@@ -2593,29 +2345,29 @@
"CoderSessionToken": []
}
]
- },
- "post": {
- "consumes": ["application/json"],
+ }
+ },
+ "/api/v2/chats/{chat}/diff": {
+ "get": {
"produces": ["application/json"],
- "tags": ["AI Providers"],
- "summary": "Create an AI provider",
- "operationId": "create-an-ai-provider",
+ "tags": ["Chats"],
+ "summary": "Get chat diff contents",
+ "operationId": "get-chat-diff-contents",
"parameters": [
{
- "description": "Create AI provider request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.CreateAIProviderRequest"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
}
],
"responses": {
- "201": {
- "description": "Created",
+ "200": {
+ "description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AIProvider"
+ "$ref": "#/definitions/codersdk.ChatDiffContents"
}
}
},
@@ -2626,17 +2378,18 @@
]
}
},
- "/api/v2/ai/providers/{idOrName}": {
- "get": {
+ "/api/v2/chats/{chat}/interrupt": {
+ "post": {
"produces": ["application/json"],
- "tags": ["AI Providers"],
- "summary": "Get an AI provider",
- "operationId": "get-an-ai-provider",
+ "tags": ["Chats"],
+ "summary": "Interrupt chat",
+ "operationId": "interrupt-chat",
"parameters": [
{
"type": "string",
- "description": "Provider ID or name",
- "name": "idOrName",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
"in": "path",
"required": true
}
@@ -2645,7 +2398,7 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AIProvider"
+ "$ref": "#/definitions/codersdk.Chat"
}
}
},
@@ -2654,52 +2407,78 @@
"CoderSessionToken": []
}
]
- },
- "delete": {
- "tags": ["AI Providers"],
- "summary": "Delete an AI provider",
- "operationId": "delete-an-ai-provider",
+ }
+ },
+ "/api/v2/chats/{chat}/messages": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "List chat messages",
+ "operationId": "list-chat-messages",
"parameters": [
{
"type": "string",
- "description": "Provider ID or name",
- "name": "idOrName",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
"in": "path",
"required": true
- }
- ],
- "responses": {
- "204": {
- "description": "No Content"
- }
- },
+ },
+ {
+ "type": "integer",
+ "description": "Return messages with id \u003c before_id",
+ "name": "before_id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Return messages with id \u003e after_id",
+ "name": "after_id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page size, 1 to 200. Defaults to 50.",
+ "name": "limit",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatMessagesResponse"
+ }
+ }
+ },
"security": [
{
"CoderSessionToken": []
}
]
},
- "patch": {
+ "post": {
"consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["AI Providers"],
- "summary": "Update an AI provider",
- "operationId": "update-an-ai-provider",
+ "tags": ["Chats"],
+ "summary": "Send chat message",
+ "operationId": "send-chat-message",
"parameters": [
{
"type": "string",
- "description": "Provider ID or name",
- "name": "idOrName",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
"in": "path",
"required": true
},
{
- "description": "Update AI provider request",
+ "description": "Create chat message request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateAIProviderRequest"
+ "$ref": "#/definitions/codersdk.CreateChatMessageRequest"
}
}
],
@@ -2707,7 +2486,7 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AIProvider"
+ "$ref": "#/definitions/codersdk.CreateChatMessageResponse"
}
}
},
@@ -2718,17 +2497,44 @@
]
}
},
- "/api/v2/appearance": {
- "get": {
+ "/api/v2/chats/{chat}/messages/{message}": {
+ "patch": {
+ "consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Get appearance",
- "operationId": "get-appearance",
+ "tags": ["Chats"],
+ "summary": "Edit chat message",
+ "operationId": "edit-chat-message",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "Message ID",
+ "name": "message",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Edit chat message request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.EditChatMessageRequest"
+ }
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AppearanceConfig"
+ "$ref": "#/definitions/codersdk.EditChatMessageResponse"
}
}
},
@@ -2737,29 +2543,36 @@
"CoderSessionToken": []
}
]
- },
- "put": {
- "consumes": ["application/json"],
+ }
+ },
+ "/api/v2/chats/{chat}/prompts": {
+ "get": {
+ "description": "Returns the user-authored prompts in a chat, newest first,\nwith each prompt's text parts concatenated in the order they\nwere authored. Used by the composer to power the up/down\narrow prompt-history cycle without paging through every\nmessage in the chat.",
"produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Update appearance",
- "operationId": "update-appearance",
+ "tags": ["Chats"],
+ "summary": "List chat user prompts",
+ "operationId": "list-chat-user-prompts",
"parameters": [
{
- "description": "Update appearance request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.UpdateAppearanceConfig"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "Page size, 0 to 2000. 0 (the default) means the server-side default of 500.",
+ "name": "limit",
+ "in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.UpdateAppearanceConfig"
+ "$ref": "#/definitions/codersdk.ChatPromptsResponse"
}
}
},
@@ -2770,22 +2583,31 @@
]
}
},
- "/api/v2/applications/auth-redirect": {
- "get": {
- "tags": ["Applications"],
- "summary": "Redirect to URI with encrypted API key",
- "operationId": "redirect-to-uri-with-encrypted-api-key",
+ "/api/v2/chats/{chat}/queue/{queuedMessage}": {
+ "delete": {
+ "tags": ["Chats"],
+ "summary": "Delete chat queued message",
+ "operationId": "delete-chat-queued-message",
"parameters": [
{
"type": "string",
- "description": "Redirect destination",
- "name": "redirect_uri",
- "in": "query"
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "Queued message ID",
+ "name": "queuedMessage",
+ "in": "path",
+ "required": true
}
],
"responses": {
- "307": {
- "description": "Temporary Redirect"
+ "204": {
+ "description": "No Content"
}
},
"security": [
@@ -2795,18 +2617,34 @@
]
}
},
- "/api/v2/applications/host": {
- "get": {
+ "/api/v2/chats/{chat}/queue/{queuedMessage}/promote": {
+ "post": {
"produces": ["application/json"],
- "tags": ["Applications"],
- "summary": "Get applications host",
- "operationId": "get-applications-host",
- "deprecated": true,
+ "tags": ["Chats"],
+ "summary": "Promote chat queued message",
+ "operationId": "promote-chat-queued-message",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "Queued message ID",
+ "name": "queuedMessage",
+ "in": "path",
+ "required": true
+ }
+ ],
"responses": {
- "200": {
- "description": "OK",
+ "202": {
+ "description": "Accepted",
"schema": {
- "$ref": "#/definitions/codersdk.AppHostResponse"
+ "$ref": "#/definitions/codersdk.Response"
}
}
},
@@ -2817,29 +2655,27 @@
]
}
},
- "/api/v2/applications/reconnecting-pty-signed-token": {
+ "/api/v2/chats/{chat}/reconcile-invalid": {
"post": {
- "consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Issue signed app token for reconnecting PTY",
- "operationId": "issue-signed-app-token-for-reconnecting-pty",
+ "tags": ["Chats"],
+ "summary": "Reconcile invalid chat state",
+ "operationId": "reconcile-invalid-chat-state",
"parameters": [
{
- "description": "Issue reconnecting PTY signed token request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.IssueReconnectingPTYSignedTokenRequest"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.IssueReconnectingPTYSignedTokenResponse"
+ "$ref": "#/definitions/codersdk.Chat"
}
}
},
@@ -2847,36 +2683,28 @@
{
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/v2/audit": {
+ "/api/v2/chats/{chat}/stream": {
"get": {
"produces": ["application/json"],
- "tags": ["Audit"],
- "summary": "Get audit logs",
- "operationId": "get-audit-logs",
+ "tags": ["Chats"],
+ "summary": "Stream chat events via WebSockets",
+ "operationId": "stream-chat-events-via-websockets",
"parameters": [
{
"type": "string",
- "description": "Search query",
- "name": "q",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Page limit",
- "name": "limit",
- "in": "query",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
"required": true
},
{
"type": "integer",
- "description": "Page offset",
- "name": "offset",
+ "description": "Skip snapshot messages with id at or before this cursor",
+ "name": "after_id",
"in": "query"
}
],
@@ -2884,7 +2712,10 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AuditLogResponse"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.ChatStreamEvent"
+ }
}
}
},
@@ -2895,77 +2726,95 @@
]
}
},
- "/api/v2/audit/testgenerate": {
- "post": {
- "consumes": ["application/json"],
- "tags": ["Audit"],
- "summary": "Generate fake audit log",
- "operationId": "generate-fake-audit-log",
+ "/api/v2/chats/{chat}/stream/git": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Watch chat workspace git state via WebSockets",
+ "operationId": "watch-chat-workspace-git-state-via-websockets",
"parameters": [
{
- "description": "Audit log request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.CreateTestAuditLogRequest"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
}
],
"responses": {
- "204": {
- "description": "No Content"
- }
- },
- "security": [
- {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.WorkspaceAgentGitServerMessage"
+ }
+ }
+ },
+ "security": [
+ {
"CoderSessionToken": []
}
- ],
- "x-apidocgen": {
- "skip": true
- }
+ ]
}
},
- "/api/v2/auth/scopes": {
+ "/api/v2/chats/{chat}/stream/parts": {
"get": {
"produces": ["application/json"],
- "tags": ["Authorization"],
- "summary": "List API key scopes",
- "operationId": "list-api-key-scopes",
+ "tags": ["Chats"],
+ "summary": "Stream chat parts via WebSockets",
+ "operationId": "stream-chat-parts-via-websockets",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.ExternalAPIKeyScopes"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.ChatStreamEvent"
+ }
}
}
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
}
}
},
- "/api/v2/authcheck": {
+ "/api/v2/chats/{chat}/title/propose": {
"post": {
- "consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Authorization"],
- "summary": "Check authorization",
- "operationId": "check-authorization",
+ "tags": ["Chats"],
+ "summary": "Propose chat title",
+ "operationId": "propose-chat-title",
"parameters": [
{
- "description": "Authorization request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.AuthorizationRequest"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.AuthorizationResponse"
+ "$ref": "#/definitions/codersdk.ProposeChatTitleResponse"
}
}
},
@@ -2976,20 +2825,41 @@
]
}
},
- "/api/v2/buildinfo": {
- "get": {
- "produces": ["application/json"],
- "tags": ["General"],
- "summary": "Build info",
- "operationId": "build-info",
- "responses": {
- "200": {
- "description": "OK",
+ "/api/v2/chats/{chat}/tool-results": {
+ "post": {
+ "consumes": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Submit chat tool results",
+ "operationId": "submit-chat-tool-results",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Chat ID",
+ "name": "chat",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Request body",
+ "name": "request",
+ "in": "body",
+ "required": true,
"schema": {
- "$ref": "#/definitions/codersdk.BuildInfoResponse"
+ "$ref": "#/definitions/codersdk.SubmitToolResultsRequest"
}
}
- }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
}
},
"/api/v2/connectionlog": {
@@ -4581,6 +4451,40 @@
]
}
},
+ "/api/v2/mcp/servers/{mcpServer}/oauth2/disconnect": {
+ "delete": {
+ "produces": ["application/json"],
+ "tags": ["MCP"],
+ "summary": "Disconnect MCP server OAuth2 token",
+ "operationId": "disconnect-mcp-server-oauth2-token",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpServer",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.MCPServerOAuth2DisconnectResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
"/api/v2/notifications/custom": {
"post": {
"consumes": ["application/json"],
@@ -5059,26 +4963,721 @@
"parameters": [
{
"type": "string",
- "description": "App ID",
- "name": "app",
+ "description": "App ID",
+ "name": "app",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Update an OAuth2 application.",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.PutOAuth2ProviderAppRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.OAuth2ProviderApp"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "delete": {
+ "tags": ["Enterprise"],
+ "summary": "Delete OAuth2 application.",
+ "operationId": "delete-oauth2-application",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "App ID",
+ "name": "app",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/oauth2-provider/apps/{app}/secrets": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Enterprise"],
+ "summary": "Get OAuth2 application secrets.",
+ "operationId": "get-oauth2-application-secrets",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "App ID",
+ "name": "app",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.OAuth2ProviderAppSecret"
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "post": {
+ "produces": ["application/json"],
+ "tags": ["Enterprise"],
+ "summary": "Create OAuth2 application secret.",
+ "operationId": "create-oauth2-application-secret",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "App ID",
+ "name": "app",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.OAuth2ProviderAppSecretFull"
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/oauth2-provider/apps/{app}/secrets/{secretID}": {
+ "delete": {
+ "tags": ["Enterprise"],
+ "summary": "Delete OAuth2 application secret.",
+ "operationId": "delete-oauth2-application-secret",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "App ID",
+ "name": "app",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Secret ID",
+ "name": "secretID",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/oauth2-provider/settings": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Enterprise"],
+ "summary": "Get OAuth2 provider settings.",
+ "operationId": "get-oauth2-provider-settings",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "put": {
+ "consumes": ["application/json"],
+ "produces": ["application/json"],
+ "tags": ["Enterprise"],
+ "summary": "Update OAuth2 provider settings.",
+ "operationId": "update-oauth2-provider-settings",
+ "parameters": [
+ {
+ "description": "OAuth2 provider settings request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/organizations": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Organizations"],
+ "summary": "Get organizations",
+ "operationId": "get-organizations",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.Organization"
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "post": {
+ "consumes": ["application/json"],
+ "produces": ["application/json"],
+ "tags": ["Organizations"],
+ "summary": "Create organization",
+ "operationId": "create-organization",
+ "parameters": [
+ {
+ "description": "Create organization request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateOrganizationRequest"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Organization"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/organizations/{organization}": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Organizations"],
+ "summary": "Get organization by ID",
+ "operationId": "get-organization-by-id",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Organization ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Organization"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "delete": {
+ "produces": ["application/json"],
+ "tags": ["Organizations"],
+ "summary": "Delete organization",
+ "operationId": "delete-organization",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization ID or name",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "patch": {
+ "consumes": ["application/json"],
+ "produces": ["application/json"],
+ "tags": ["Organizations"],
+ "summary": "Update organization",
+ "operationId": "update-organization",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization ID or name",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Patch organization request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateOrganizationRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Organization"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/organizations/{organization}/ai/spend/export": {
+ "get": {
+ "description": "Returns per-user, per-group, per-model, per-provider aggregated AI spend for the organization as CSV, built from raw AI Gateway token usage.\nThe optional period_start and period_end query parameters bound the period and are interpreted as UTC. They must be provided together and span at most 31 days. When both are omitted, the current UTC monthly period is used.\nAn explicit period_start must fall within the configured AI Gateway data retention window, since older token usage is purged. The default period is narrowed to that window instead, and every row echoes the applied bounds.\nRequires organization-level administrator permissions.",
+ "produces": ["text/csv"],
+ "tags": ["Enterprise"],
+ "summary": "Export organization AI spend as CSV",
+ "operationId": "export-organization-ai-spend-as-csv",
+ "parameters": [
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Organization ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "format": "date-time",
+ "description": "Inclusive lower bound (RFC3339)",
+ "name": "period_start",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "format": "date-time",
+ "description": "Exclusive upper bound (RFC3339)",
+ "name": "period_end",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/organizations/{organization}/chats/model-overrides": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "List organization chat model overrides",
+ "operationId": "list-organization-chat-model-overrides",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatModelOverridesResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/v2/organizations/{organization}/chats/model-overrides/{context}": {
+ "put": {
+ "consumes": ["application/json"],
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Update organization chat model override",
+ "operationId": "update-organization-chat-model-override",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "enum": [
+ "general",
+ "explore",
+ "title_generation",
+ "compaction",
+ "advisor"
+ ],
+ "type": "string",
+ "description": "Override context",
+ "name": "context",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Model override",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateChatModelOverrideRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatModelOverrideResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/v2/organizations/{organization}/chats/models": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "List AI models and provider descriptors in an organization",
+ "operationId": "list-ai-models-and-provider-descriptors-in-an-organization",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.OrganizationChatModelsResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "post": {
+ "consumes": ["application/json"],
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Create an AI model in an organization",
+ "operationId": "create-an-ai-model-in-an-organization",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Model",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateChatModelRequest"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatModel"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/v2/organizations/{organization}/chats/models/{model}": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Get an AI model",
+ "operationId": "get-an-ai-model",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Model ID",
+ "name": "model",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatModel"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "delete": {
+ "tags": ["Chats"],
+ "summary": "Delete an AI model",
+ "operationId": "delete-an-ai-model",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Model ID",
+ "name": "model",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "patch": {
+ "consumes": ["application/json"],
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Update an AI model",
+ "operationId": "update-an-ai-model",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Model ID",
+ "name": "model",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Model updates",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateChatModelRequest"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.ChatModel"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/v2/organizations/{organization}/chats/models/{model}/acl": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Get an AI model ACL",
+ "operationId": "get-an-ai-model-acl",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
"in": "path",
"required": true
},
{
- "description": "Update an OAuth2 application.",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.PutOAuth2ProviderAppRequest"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Model ID",
+ "name": "model",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderApp"
+ "$ref": "#/definitions/codersdk.ChatModelACL"
}
}
},
@@ -5086,19 +5685,40 @@
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
},
- "delete": {
- "tags": ["Enterprise"],
- "summary": "Delete OAuth2 application.",
- "operationId": "delete-oauth2-application",
+ "patch": {
+ "consumes": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Update an AI model ACL",
+ "operationId": "update-an-ai-model-acl",
"parameters": [
{
"type": "string",
- "description": "App ID",
- "name": "app",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "Model ID",
+ "name": "model",
"in": "path",
"required": true
+ },
+ {
+ "description": "Sparse model ACL update",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateChatModelACLRequest"
+ }
}
],
"responses": {
@@ -5110,20 +5730,24 @@
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/oauth2-provider/apps/{app}/secrets": {
+ "/api/v2/organizations/{organization}/groups": {
"get": {
"produces": ["application/json"],
"tags": ["Enterprise"],
- "summary": "Get OAuth2 application secrets.",
- "operationId": "get-oauth2-application-secrets",
+ "summary": "Get groups by organization",
+ "operationId": "get-groups-by-organization",
"parameters": [
{
"type": "string",
- "description": "App ID",
- "name": "app",
+ "format": "uuid",
+ "description": "Organization ID",
+ "name": "organization",
"in": "path",
"required": true
}
@@ -5134,7 +5758,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderAppSecret"
+ "$ref": "#/definitions/codersdk.Group"
}
}
}
@@ -5146,27 +5770,34 @@
]
},
"post": {
+ "consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Enterprise"],
- "summary": "Create OAuth2 application secret.",
- "operationId": "create-oauth2-application-secret",
+ "summary": "Create group for organization",
+ "operationId": "create-group-for-organization",
"parameters": [
+ {
+ "description": "Create group request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateGroupRequest"
+ }
+ },
{
"type": "string",
- "description": "App ID",
- "name": "app",
+ "description": "Organization ID",
+ "name": "organization",
"in": "path",
"required": true
}
],
"responses": {
- "200": {
- "description": "OK",
+ "201": {
+ "description": "Created",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderAppSecretFull"
- }
+ "$ref": "#/definitions/codersdk.Group"
}
}
},
@@ -5177,50 +5808,35 @@
]
}
},
- "/api/v2/oauth2-provider/apps/{app}/secrets/{secretID}": {
- "delete": {
+ "/api/v2/organizations/{organization}/groups/ai/spend": {
+ "get": {
+ "description": "Returns AI spend limits and aggregate spend for the requested groups.\nA maximum of 100 group IDs may be requested per call, and requests with more are rejected, so callers are expected to batch across multiple requests.\nUnknown or unreadable group IDs are silently omitted.",
+ "produces": ["application/json"],
"tags": ["Enterprise"],
- "summary": "Delete OAuth2 application secret.",
- "operationId": "delete-oauth2-application-secret",
+ "summary": "Get organization groups AI spend",
+ "operationId": "get-organization-groups-ai-spend",
"parameters": [
{
"type": "string",
- "description": "App ID",
- "name": "app",
+ "format": "uuid",
+ "description": "Organization ID",
+ "name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
- "description": "Secret ID",
- "name": "secretID",
- "in": "path",
+ "description": "Comma-separated list of group IDs (maximum 100)",
+ "name": "group_ids",
+ "in": "query",
"required": true
}
],
- "responses": {
- "204": {
- "description": "No Content"
- }
- },
- "security": [
- {
- "CoderSessionToken": []
- }
- ]
- }
- },
- "/api/v2/oauth2-provider/settings": {
- "get": {
- "produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Get OAuth2 provider settings.",
- "operationId": "get-oauth2-provider-settings",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
+ "$ref": "#/definitions/codersdk.OrganizationGroupsAISpend"
}
}
},
@@ -5229,29 +5845,36 @@
"CoderSessionToken": []
}
]
- },
- "put": {
- "consumes": ["application/json"],
+ }
+ },
+ "/api/v2/organizations/{organization}/groups/{groupName}": {
+ "get": {
"produces": ["application/json"],
"tags": ["Enterprise"],
- "summary": "Update OAuth2 provider settings.",
- "operationId": "update-oauth2-provider-settings",
+ "summary": "Get group by organization and group name",
+ "operationId": "get-group-by-organization-and-group-name",
"parameters": [
{
- "description": "OAuth2 provider settings request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Organization ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Group name",
+ "name": "groupName",
+ "in": "path",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.OAuth2ProviderSettings"
+ "$ref": "#/definitions/codersdk.Group"
}
}
},
@@ -5262,51 +5885,59 @@
]
}
},
- "/api/v2/organizations": {
+ "/api/v2/organizations/{organization}/groups/{groupName}/members": {
"get": {
"produces": ["application/json"],
- "tags": ["Organizations"],
- "summary": "Get organizations",
- "operationId": "get-organizations",
- "responses": {
- "200": {
- "description": "OK",
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.Organization"
- }
- }
- }
- },
- "security": [
- {
- "CoderSessionToken": []
- }
- ]
- },
- "post": {
- "consumes": ["application/json"],
- "produces": ["application/json"],
- "tags": ["Organizations"],
- "summary": "Create organization",
- "operationId": "create-organization",
+ "tags": ["Enterprise"],
+ "summary": "Get group members by organization and group name",
+ "operationId": "get-group-members-by-organization-and-group-name",
"parameters": [
{
- "description": "Create organization request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.CreateOrganizationRequest"
- }
+ "type": "string",
+ "format": "uuid",
+ "description": "Organization ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Group name",
+ "name": "groupName",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Member search query",
+ "name": "q",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "After ID",
+ "name": "after_id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page limit",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Page offset",
+ "name": "offset",
+ "in": "query"
}
],
"responses": {
- "201": {
- "description": "Created",
+ "200": {
+ "description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Organization"
+ "$ref": "#/definitions/codersdk.GroupMembersResponse"
}
}
},
@@ -5317,12 +5948,13 @@
]
}
},
- "/api/v2/organizations/{organization}": {
+ "/api/v2/organizations/{organization}/groups/{groupName}/members/ai/spend": {
"get": {
+ "description": "Returns aggregate AI spend attributed to the group per requested user.\nA maximum of 100 user IDs may be requested per call, and requests with more are rejected, so callers are expected to batch across multiple requests.\nUser IDs that are not members of the group, or that the caller has no read access to, are silently omitted.",
"produces": ["application/json"],
- "tags": ["Organizations"],
- "summary": "Get organization by ID",
- "operationId": "get-organization-by-id",
+ "tags": ["Enterprise"],
+ "summary": "Get group members AI spend by organization",
+ "operationId": "get-group-members-ai-spend-by-organization",
"parameters": [
{
"type": "string",
@@ -5331,13 +5963,27 @@
"name": "organization",
"in": "path",
"required": true
+ },
+ {
+ "type": "string",
+ "description": "Group name",
+ "name": "groupName",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Comma-separated list of user IDs (maximum 100)",
+ "name": "user_ids",
+ "in": "query",
+ "required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Organization"
+ "$ref": "#/definitions/codersdk.GroupMembersAISpend"
}
}
},
@@ -5346,16 +5992,18 @@
"CoderSessionToken": []
}
]
- },
- "delete": {
+ }
+ },
+ "/api/v2/organizations/{organization}/mcp-servers": {
+ "get": {
"produces": ["application/json"],
- "tags": ["Organizations"],
- "summary": "Delete organization",
- "operationId": "delete-organization",
+ "tags": ["MCP"],
+ "summary": "List MCP server configs",
+ "operationId": "list-mcp-server-configs",
"parameters": [
{
"type": "string",
- "description": "Organization ID or name",
+ "description": "Organization name or ID",
"name": "organization",
"in": "path",
"required": true
@@ -5365,7 +6013,10 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.MCPServerConfig"
+ }
}
}
},
@@ -5373,37 +6024,40 @@
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
},
- "patch": {
+ "post": {
"consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Organizations"],
- "summary": "Update organization",
- "operationId": "update-organization",
+ "tags": ["MCP"],
+ "summary": "Create MCP server config",
+ "operationId": "create-mcp-server-config",
"parameters": [
{
"type": "string",
- "description": "Organization ID or name",
+ "description": "Organization name or ID",
"name": "organization",
"in": "path",
"required": true
},
{
- "description": "Patch organization request",
+ "description": "Create MCP server config request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateOrganizationRequest"
+ "$ref": "#/definitions/codersdk.CreateMCPServerConfigRequest"
}
}
],
"responses": {
- "200": {
- "description": "OK",
+ "201": {
+ "description": "Created",
"schema": {
- "$ref": "#/definitions/codersdk.Organization"
+ "$ref": "#/definitions/codersdk.MCPServerConfig"
}
}
},
@@ -5411,64 +6065,31 @@
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/organizations/{organization}/ai/spend/export": {
+ "/api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig}": {
"get": {
- "description": "Returns per-user, per-group, per-model, per-provider aggregated AI spend for the organization as CSV, built from raw AI Gateway token usage.\nThe optional period_start and period_end query parameters bound the period and are interpreted as UTC. They must be provided together and span at most 31 days. When both are omitted, the current UTC monthly period is used.\nAn explicit period_start must fall within the configured AI Gateway data retention window, since older token usage is purged. The default period is narrowed to that window instead, and every row echoes the applied bounds.\nRequires organization-level administrator permissions.",
- "produces": ["text/csv"],
- "tags": ["Enterprise"],
- "summary": "Export organization AI spend as CSV",
- "operationId": "export-organization-ai-spend-as-csv",
+ "produces": ["application/json"],
+ "tags": ["MCP"],
+ "summary": "Get MCP server config",
+ "operationId": "get-mcp-server-config",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Organization ID",
+ "description": "Organization name or ID",
"name": "organization",
"in": "path",
"required": true
},
- {
- "type": "string",
- "format": "date-time",
- "description": "Inclusive lower bound (RFC3339)",
- "name": "period_start",
- "in": "query"
- },
- {
- "type": "string",
- "format": "date-time",
- "description": "Exclusive upper bound (RFC3339)",
- "name": "period_end",
- "in": "query"
- }
- ],
- "responses": {
- "200": {
- "description": "OK"
- }
- },
- "security": [
- {
- "CoderSessionToken": []
- }
- ]
- }
- },
- "/api/v2/organizations/{organization}/groups": {
- "get": {
- "produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Get groups by organization",
- "operationId": "get-groups-by-organization",
- "parameters": [
{
"type": "string",
"format": "uuid",
- "description": "Organization ID",
- "name": "organization",
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
"in": "path",
"required": true
}
@@ -5477,10 +6098,7 @@
"200": {
"description": "OK",
"schema": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/codersdk.Group"
- }
+ "$ref": "#/definitions/codersdk.MCPServerConfig"
}
}
},
@@ -5488,76 +6106,83 @@
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
},
- "post": {
- "consumes": ["application/json"],
- "produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Create group for organization",
- "operationId": "create-group-for-organization",
+ "delete": {
+ "tags": ["MCP"],
+ "summary": "Delete MCP server config",
+ "operationId": "delete-mcp-server-config",
"parameters": [
{
- "description": "Create group request",
- "name": "request",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/codersdk.CreateGroupRequest"
- }
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
},
{
"type": "string",
- "description": "Organization ID",
- "name": "organization",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
"in": "path",
"required": true
}
],
"responses": {
- "201": {
- "description": "Created",
- "schema": {
- "$ref": "#/definitions/codersdk.Group"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
- }
- },
- "/api/v2/organizations/{organization}/groups/ai/spend": {
- "get": {
- "description": "Returns AI spend limits and aggregate spend for the requested groups.\nA maximum of 100 group IDs may be requested per call, and requests with more are rejected, so callers are expected to batch across multiple requests.\nUnknown or unreadable group IDs are silently omitted.",
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "patch": {
+ "consumes": ["application/json"],
"produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Get organization groups AI spend",
- "operationId": "get-organization-groups-ai-spend",
+ "tags": ["MCP"],
+ "summary": "Update MCP server config",
+ "operationId": "update-mcp-server-config",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Organization ID",
+ "description": "Organization name or ID",
"name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
- "description": "Comma-separated list of group IDs (maximum 100)",
- "name": "group_ids",
- "in": "query",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
+ "in": "path",
"required": true
+ },
+ {
+ "description": "Update MCP server config request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateMCPServerConfigRequest"
+ }
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.OrganizationGroupsAISpend"
+ "$ref": "#/definitions/codersdk.MCPServerConfig"
}
}
},
@@ -5565,28 +6190,31 @@
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/organizations/{organization}/groups/{groupName}": {
+ "/api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl": {
"get": {
"produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Get group by organization and group name",
- "operationId": "get-group-by-organization-and-group-name",
+ "tags": ["MCP"],
+ "summary": "Get MCP server config ACL",
+ "operationId": "get-mcp-server-config-acl",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Organization ID",
+ "description": "Organization name or ID",
"name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
- "description": "Group name",
- "name": "groupName",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
"in": "path",
"required": true
}
@@ -5595,7 +6223,7 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Group"
+ "$ref": "#/definitions/codersdk.MCPServerConfigACL"
}
}
},
@@ -5603,116 +6231,92 @@
{
"CoderSessionToken": []
}
- ]
- }
- },
- "/api/v2/organizations/{organization}/groups/{groupName}/members": {
- "get": {
- "produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Get group members by organization and group name",
- "operationId": "get-group-members-by-organization-and-group-name",
- "parameters": [
- {
- "type": "string",
- "format": "uuid",
- "description": "Organization ID",
- "name": "organization",
- "in": "path",
- "required": true
- },
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ },
+ "patch": {
+ "consumes": ["application/json"],
+ "tags": ["MCP"],
+ "summary": "Update MCP server config ACL",
+ "operationId": "update-mcp-server-config-acl",
+ "parameters": [
{
"type": "string",
- "description": "Group name",
- "name": "groupName",
+ "description": "Organization name or ID",
+ "name": "organization",
"in": "path",
"required": true
},
- {
- "type": "string",
- "description": "Member search query",
- "name": "q",
- "in": "query"
- },
{
"type": "string",
"format": "uuid",
- "description": "After ID",
- "name": "after_id",
- "in": "query"
- },
- {
- "type": "integer",
- "description": "Page limit",
- "name": "limit",
- "in": "query"
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
+ "in": "path",
+ "required": true
},
{
- "type": "integer",
- "description": "Page offset",
- "name": "offset",
- "in": "query"
+ "description": "Update MCP server config ACL request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateMCPServerConfigACLRequest"
+ }
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.GroupMembersResponse"
- }
+ "204": {
+ "description": "No Content"
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
- "/api/v2/organizations/{organization}/groups/{groupName}/members/ai/spend": {
+ "/api/v2/organizations/{organization}/mcp-servers/{mcpserverconfig}/oauth2/connect": {
"get": {
- "description": "Returns aggregate AI spend attributed to the group per requested user.\nA maximum of 100 user IDs may be requested per call, and requests with more are rejected, so callers are expected to batch across multiple requests.\nUser IDs that are not members of the group, or that the caller has no read access to, are silently omitted.",
- "produces": ["application/json"],
- "tags": ["Enterprise"],
- "summary": "Get group members AI spend by organization",
- "operationId": "get-group-members-ai-spend-by-organization",
+ "tags": ["MCP"],
+ "summary": "Initiate MCP server OAuth2 connect",
+ "operationId": "initiate-mcp-server-oauth2-connect",
"parameters": [
{
"type": "string",
- "format": "uuid",
- "description": "Organization ID",
+ "description": "Organization name or ID",
"name": "organization",
"in": "path",
"required": true
},
{
"type": "string",
- "description": "Group name",
- "name": "groupName",
+ "format": "uuid",
+ "description": "MCP server config ID",
+ "name": "mcpserverconfig",
"in": "path",
"required": true
- },
- {
- "type": "string",
- "description": "Comma-separated list of user IDs (maximum 100)",
- "name": "user_ids",
- "in": "query",
- "required": true
}
],
"responses": {
- "200": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/codersdk.GroupMembersAISpend"
- }
+ "307": {
+ "description": "Temporary Redirect"
}
},
"security": [
{
"CoderSessionToken": []
}
- ]
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
}
},
"/api/v2/organizations/{organization}/members": {
@@ -6011,6 +6615,100 @@
]
}
},
+ "/api/v2/organizations/{organization}/members/{user}/chats/model-overrides": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Get organization member chat model overrides",
+ "operationId": "get-organization-member-chat-model-overrides",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "User name, ID, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.UserChatPersonalModelOverridesResponse"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
+ "/api/v2/organizations/{organization}/members/{user}/chats/model-overrides/{context}": {
+ "put": {
+ "consumes": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Update organization member chat model override",
+ "operationId": "update-organization-member-chat-model-override",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Organization name or ID",
+ "name": "organization",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "User name, ID, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ },
+ {
+ "enum": ["root", "general", "explore"],
+ "type": "string",
+ "description": "Override context",
+ "name": "context",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Personal model override",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.UpdateUserChatPersonalModelOverrideRequest"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
"/api/v2/organizations/{organization}/members/{user}/roles": {
"put": {
"consumes": ["application/json"],
@@ -9595,9 +10293,97 @@
"/api/v2/users/{user}": {
"get": {
"produces": ["application/json"],
- "tags": ["Users"],
- "summary": "Get user by name",
- "operationId": "get-user-by-name",
+ "tags": ["Users"],
+ "summary": "Get user by name",
+ "operationId": "get-user-by-name",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.User"
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ },
+ "delete": {
+ "tags": ["Users"],
+ "summary": "Delete user",
+ "operationId": "delete-user",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "User ID, name, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/users/{user}/ai-provider-keys": {
+ "get": {
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "List user AI provider key configurations",
+ "operationId": "list-user-ai-provider-key-configurations",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "User ID, username, or me",
+ "name": "user",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.UserAIProviderKeyConfig"
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ]
+ }
+ },
+ "/api/v2/users/{user}/ai-provider-keys/{aiProvider}": {
+ "put": {
+ "consumes": ["application/json"],
+ "produces": ["application/json"],
+ "tags": ["Chats"],
+ "summary": "Update user AI provider key",
+ "operationId": "update-user-ai-provider-key",
"parameters": [
{
"type": "string",
@@ -9605,13 +10391,30 @@
"name": "user",
"in": "path",
"required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "AI provider ID",
+ "name": "aiProvider",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Request body",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.CreateUserAIProviderKeyRequest"
+ }
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.User"
+ "$ref": "#/definitions/codersdk.UserAIProviderKeyConfig"
}
}
},
@@ -9622,21 +10425,29 @@
]
},
"delete": {
- "tags": ["Users"],
- "summary": "Delete user",
- "operationId": "delete-user",
+ "tags": ["Chats"],
+ "summary": "Delete user AI provider key",
+ "operationId": "delete-user-ai-provider-key",
"parameters": [
{
"type": "string",
- "description": "User ID, name, or me",
+ "description": "User ID, username, or me",
"name": "user",
"in": "path",
"required": true
+ },
+ {
+ "type": "string",
+ "format": "uuid",
+ "description": "AI provider ID",
+ "name": "aiProvider",
+ "in": "path",
+ "required": true
}
],
"responses": {
- "200": {
- "description": "OK"
+ "204": {
+ "description": "No Content"
}
},
"security": [
@@ -14354,6 +15165,12 @@
"enum": ["prebuild_claimed"],
"x-enum-varnames": ["ReinitializeReasonPrebuildClaimed"]
},
+ "coderd.chatsByWorkspaceResponse": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
"coderd.cspViolation": {
"type": "object",
"properties": {
@@ -15139,6 +15956,33 @@
}
}
},
+ "codersdk.AIProviderSummary": {
+ "type": "object",
+ "properties": {
+ "deleted": {
+ "type": "boolean"
+ },
+ "display_name": {
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "icon": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "$ref": "#/definitions/codersdk.AIProviderType"
+ }
+ }
+ },
"codersdk.AIProviderType": {
"type": "string",
"enum": [
@@ -16521,6 +17365,14 @@
}
}
},
+ "codersdk.ChatAutoArchiveDaysResponse": {
+ "type": "object",
+ "properties": {
+ "auto_archive_days": {
+ "type": "integer"
+ }
+ }
+ },
"codersdk.ChatBusyBehavior": {
"type": "string",
"enum": ["queue", "interrupt"],
@@ -16676,6 +17528,25 @@
}
}
},
+ "codersdk.ChatDebugLoggingAdminSettings": {
+ "type": "object",
+ "properties": {
+ "allow_users": {
+ "type": "boolean"
+ },
+ "forced_by_deployment": {
+ "type": "boolean"
+ }
+ }
+ },
+ "codersdk.ChatDebugRetentionDaysResponse": {
+ "type": "object",
+ "properties": {
+ "debug_retention_days": {
+ "type": "integer"
+ }
+ }
+ },
"codersdk.ChatDiffContents": {
"type": "object",
"properties": {
@@ -17877,11 +18748,27 @@
"ChatPersonalModelOverrideModeModel"
]
},
+ "codersdk.ChatPersonalModelOverridesAdminSettings": {
+ "type": "object",
+ "properties": {
+ "allow_users": {
+ "type": "boolean"
+ }
+ }
+ },
"codersdk.ChatPlanMode": {
"type": "string",
"enum": ["plan"],
"x-enum-varnames": ["ChatPlanModePlan"]
},
+ "codersdk.ChatPlanModeInstructionsResponse": {
+ "type": "object",
+ "properties": {
+ "plan_mode_instructions": {
+ "type": "string"
+ }
+ }
+ },
"codersdk.ChatPrompt": {
"type": "object",
"properties": {
@@ -18112,6 +18999,20 @@
}
}
},
+ "codersdk.ChatSystemPromptResponse": {
+ "type": "object",
+ "properties": {
+ "default_system_prompt": {
+ "type": "string"
+ },
+ "include_default_system_prompt": {
+ "type": "boolean"
+ },
+ "system_prompt": {
+ "type": "string"
+ }
+ }
+ },
"codersdk.ChatUnsupportedProvider": {
"type": "object",
"properties": {
@@ -18194,6 +19095,15 @@
"ChatWatchEventKindContextDirty"
]
},
+ "codersdk.ChatWorkspaceTTLResponse": {
+ "type": "object",
+ "properties": {
+ "workspace_ttl_ms": {
+ "description": "WorkspaceTTLMillis is the workspace TTL in milliseconds.\nZero means disabled; the template's own autostop setting applies.",
+ "type": "integer"
+ }
+ }
+ },
"codersdk.ClusterConfig": {
"type": "object",
"properties": {
@@ -19144,6 +20054,14 @@
}
}
},
+ "codersdk.CreateUserAIProviderKeyRequest": {
+ "type": "object",
+ "properties": {
+ "api_key": {
+ "type": "string"
+ }
+ }
+ },
"codersdk.CreateUserRequestWithOrgs": {
"type": "object",
"required": ["username"],
@@ -24295,6 +25213,17 @@
}
}
},
+ "codersdk.SubmitToolResultsRequest": {
+ "type": "object",
+ "properties": {
+ "results": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.ToolResult"
+ }
+ }
+ }
+ },
"codersdk.SupportConfig": {
"type": "object",
"properties": {
@@ -25462,6 +26391,23 @@
}
}
},
+ "codersdk.ToolResult": {
+ "type": "object",
+ "properties": {
+ "is_error": {
+ "type": "boolean"
+ },
+ "output": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tool_call_id": {
+ "type": "string"
+ }
+ }
+ },
"codersdk.TraceConfig": {
"type": "object",
"properties": {
@@ -25570,6 +26516,30 @@
}
}
},
+ "codersdk.UpdateChatAutoArchiveDaysRequest": {
+ "type": "object",
+ "properties": {
+ "auto_archive_days": {
+ "type": "integer"
+ }
+ }
+ },
+ "codersdk.UpdateChatDebugLoggingAllowUsersRequest": {
+ "type": "object",
+ "properties": {
+ "allow_users": {
+ "type": "boolean"
+ }
+ }
+ },
+ "codersdk.UpdateChatDebugRetentionDaysRequest": {
+ "type": "object",
+ "properties": {
+ "debug_retention_days": {
+ "type": "integer"
+ }
+ }
+ },
"codersdk.UpdateChatModelACLRequest": {
"type": "object",
"properties": {
@@ -25628,6 +26598,22 @@
}
}
},
+ "codersdk.UpdateChatPersonalModelOverridesAdminSettingsRequest": {
+ "type": "object",
+ "properties": {
+ "allow_users": {
+ "type": "boolean"
+ }
+ }
+ },
+ "codersdk.UpdateChatPlanModeInstructionsRequest": {
+ "type": "object",
+ "properties": {
+ "plan_mode_instructions": {
+ "type": "string"
+ }
+ }
+ },
"codersdk.UpdateChatRequest": {
"type": "object",
"properties": {
@@ -25669,6 +26655,26 @@
}
}
},
+ "codersdk.UpdateChatSystemPromptRequest": {
+ "type": "object",
+ "properties": {
+ "include_default_system_prompt": {
+ "type": "boolean"
+ },
+ "system_prompt": {
+ "type": "string"
+ }
+ }
+ },
+ "codersdk.UpdateChatWorkspaceTTLRequest": {
+ "type": "object",
+ "properties": {
+ "workspace_ttl_ms": {
+ "description": "WorkspaceTTLMillis is the workspace TTL in milliseconds.\nZero means disabled; the template's own autostop setting applies.",
+ "type": "integer"
+ }
+ }
+ },
"codersdk.UpdateCheckResponse": {
"type": "object",
"properties": {
@@ -25993,6 +26999,24 @@
}
}
},
+ "codersdk.UpdateUserChatCompactionThresholdRequest": {
+ "type": "object",
+ "properties": {
+ "threshold_percent": {
+ "type": "integer",
+ "maximum": 100,
+ "minimum": 0
+ }
+ }
+ },
+ "codersdk.UpdateUserChatDebugLoggingRequest": {
+ "type": "object",
+ "properties": {
+ "debug_logging_enabled": {
+ "type": "boolean"
+ }
+ }
+ },
"codersdk.UpdateUserChatPersonalModelOverrideRequest": {
"type": "object",
"properties": {
@@ -26407,6 +27431,23 @@
}
}
},
+ "codersdk.UserAIProviderKeyConfig": {
+ "type": "object",
+ "properties": {
+ "byok_enabled": {
+ "type": "boolean"
+ },
+ "has_provider_api_key": {
+ "type": "boolean"
+ },
+ "has_user_api_key": {
+ "type": "boolean"
+ },
+ "provider": {
+ "$ref": "#/definitions/codersdk.AIProviderSummary"
+ }
+ }
+ },
"codersdk.UserAISpendStatus": {
"type": "object",
"properties": {
@@ -26527,6 +27568,51 @@
}
}
},
+ "codersdk.UserChatCompactionThreshold": {
+ "type": "object",
+ "properties": {
+ "model_config_id": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "threshold_percent": {
+ "type": "integer"
+ }
+ }
+ },
+ "codersdk.UserChatCompactionThresholds": {
+ "type": "object",
+ "properties": {
+ "thresholds": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.UserChatCompactionThreshold"
+ }
+ }
+ }
+ },
+ "codersdk.UserChatCustomPrompt": {
+ "type": "object",
+ "properties": {
+ "custom_prompt": {
+ "type": "string"
+ }
+ }
+ },
+ "codersdk.UserChatDebugLoggingSettings": {
+ "type": "object",
+ "properties": {
+ "debug_logging_enabled": {
+ "type": "boolean"
+ },
+ "forced_by_deployment": {
+ "type": "boolean"
+ },
+ "user_toggle_allowed": {
+ "type": "boolean"
+ }
+ }
+ },
"codersdk.UserChatPersonalModelOverridesResponse": {
"type": "object",
"properties": {
diff --git a/coderd/chat_routes.go b/coderd/chat_routes.go
new file mode 100644
index 00000000000..70a942fc3a4
--- /dev/null
+++ b/coderd/chat_routes.go
@@ -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)
+ })
+}
diff --git a/coderd/chat_routes_internal_test.go b/coderd/chat_routes_internal_test.go
new file mode 100644
index 00000000000..2522911f9f9
--- /dev/null
+++ b/coderd/chat_routes_internal_test.go
@@ -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)
+ }
+}
diff --git a/coderd/chat_routes_test.go b/coderd/chat_routes_test.go
new file mode 100644
index 00000000000..5b54c307884
--- /dev/null
+++ b/coderd/chat_routes_test.go
@@ -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)
+ }
+}
diff --git a/coderd/coderd.go b/coderd/coderd.go
index 7199a8977d6..ce04b0cb94c 100644
--- a/coderd/coderd.go
+++ b/coderd/coderd.go
@@ -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
diff --git a/coderd/coderdtest/swagger_test.go b/coderd/coderdtest/swagger_test.go
index 07ea3c74400..f805264de73 100644
--- a/coderd/coderdtest/swagger_test.go
+++ b/coderd/coderdtest/swagger_test.go
@@ -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()
diff --git a/coderd/coderdtest/swaggerparser.go b/coderd/coderdtest/swaggerparser.go
index 2fe62ecd1ad..bff1ec553ab 100644
--- a/coderd/coderdtest/swaggerparser.go
+++ b/coderd/coderdtest/swaggerparser.go
@@ -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)
diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go
index a7a093e3987..9a0fadcfd51 100644
--- a/coderd/exp_chats.go
+++ b/coderd/exp_chats.go
@@ -221,16 +221,13 @@ func publishChatConfigEvent(logger slog.Logger, ps dbpubsub.Pubsub, kind pubsub.
}
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Watch chat events for a user via WebSockets
// @ID watch-chat-events-for-a-user-via-websockets
// @Security CoderSessionToken
// @Tags Chats
// @Produce json
// @Success 200 {object} codersdk.ChatWatchEvent
-// @Router /api/experimental/chats/watch [get]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/watch [get]
func (api *API) watchChats(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -311,33 +308,30 @@ func (api *API) watchChats(rw http.ResponseWriter, r *http.Request) {
<-ctx.Done()
}
-// EXPERIMENTAL: chatsByWorkspace returns a mapping of workspace ID to
// the latest non-archived chat ID for each requested workspace.
// The query returns all matching chats and RBAC post-filters them;
// the handler then picks the latest per workspace in Go. This avoids
// the DISTINCT ON + post-filter bug where the sole candidate is
// silently dropped when the caller can't read it.
//
-// TODO:
-// 1. move aggregation to a SQL view with proper in-query authz so we
-// can return a single row per workspace without this two-pass approach.
-// 2. Restore the below router annotation and un-skip docs gen
-// Router /api/experimental/chats/by-workspace [post]
-//
-// @Summary Get latest chats by workspace IDs
-// @ID get-latest-chats-by-workspace-ids
+// TODO: move aggregation to a SQL view with proper in-query authz so the
+// handler can return a single row per workspace without this two-pass approach.
+type chatsByWorkspaceResponse map[uuid.UUID]uuid.UUID
+
+// @Summary List chats by workspace
+// @ID list-chats-by-workspace
// @Security CoderSessionToken
// @Tags Chats
-// @Accept json
+// @Param workspace_ids query string false "Comma-separated workspace IDs"
// @Produce json
-// @Success 200
-// @x-apidocgen {"skip": true}
+// @Success 200 {object} chatsByWorkspaceResponse
+// @Router /api/v2/chats/by-workspace [get]
func (api *API) chatsByWorkspace(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idsParam := r.URL.Query().Get("workspace_ids")
if idsParam == "" {
- httpapi.Write(ctx, rw, http.StatusOK, map[uuid.UUID]uuid.UUID{})
+ httpapi.Write(ctx, rw, http.StatusOK, chatsByWorkspaceResponse{})
return
}
@@ -381,7 +375,7 @@ func (api *API) chatsByWorkspace(rw http.ResponseWriter, r *http.Request) {
// The SQL orders by (workspace_id, updated_at DESC), so the first
// chat seen per workspace after RBAC filtering is the latest
// readable one.
- result := make(map[uuid.UUID]uuid.UUID, len(chats))
+ result := make(chatsByWorkspaceResponse, len(chats))
for _, chat := range chats {
if chat.WorkspaceID.Valid {
if _, exists := result[chat.WorkspaceID.UUID]; !exists {
@@ -393,18 +387,18 @@ func (api *API) chatsByWorkspace(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, result)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary List chats
// @ID list-chats
// @Security CoderSessionToken
// @Tags Chats
// @Produce json
// @Param q query string false "Search query. Supports `title:` (case-insensitive, quote multi-word values), `archived:bool`, `has_unread:bool`, `pr_status:` as repeated or comma-separated values, `source:`, `diff_url:` (quote values containing colons), `pr:` (exact PR number match), `repo:` (case-insensitive substring match against git remote origin or URL), `pr_title:` (case-insensitive PR title substring), `search:` (full-text search across chat titles, PR titles, PR numbers, and message bodies; message bodies match English word stems, e.g. `refactor` matches `refactoring`, and ignore English stopwords; titles and PR titles match whole words case-insensitively without stemming; quote multi-word values; cannot be combined with title, pr_title, or pr; a value that tokenizes to no searchable words, e.g. punctuation only, returns an empty list). Bare terms are not supported; use `title:` or `search:`."
-// @Param label query string false "Filter by label as key:value. Repeat for multiple (AND logic)."
+// @Param label query []string false "Filter by label as key:value. Repeat for multiple (AND logic)." collectionFormat(multi)
+// @Param after_id query string false "After ID" format(uuid)
+// @Param limit query int false "Page limit"
+// @Param offset query int false "Page offset"
// @Success 200 {array} codersdk.Chat
-// @Router /api/experimental/chats [get]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats [get]
func (api *API) listChats(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -1186,8 +1180,6 @@ func invalidChatMCPServerIDsResponse(ids []uuid.UUID) codersdk.Response {
}
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Create chat
// @ID create-chat
// @Security CoderSessionToken
@@ -1197,8 +1189,7 @@ func invalidChatMCPServerIDsResponse(ids []uuid.UUID) codersdk.Response {
// @Param request body codersdk.CreateChatRequest true "Create chat request"
// @Success 201 {object} codersdk.Chat
// @Failure 413 {object} codersdk.Response "Request body exceeds 256 KiB"
-// @Router /api/experimental/chats [post]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats [post]
func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -1460,8 +1451,6 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusCreated, response)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Get chat by ID
// @ID get-chat-by-id
// @Security CoderSessionToken
@@ -1469,8 +1458,7 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
// @Produce json
// @Param chat path string true "Chat ID" format(uuid)
// @Success 200 {object} codersdk.Chat
-// @Router /api/experimental/chats/{chat} [get]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat} [get]
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) getChat(rw http.ResponseWriter, r *http.Request) {
@@ -1567,8 +1555,6 @@ func (api *API) getChat(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, sdkChat)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary List chat messages
// @ID list-chat-messages
// @Security CoderSessionToken
@@ -1579,8 +1565,7 @@ func (api *API) getChat(rw http.ResponseWriter, r *http.Request) {
// @Param after_id query int false "Return messages with id > after_id"
// @Param limit query int false "Page size, 1 to 200. Defaults to 50."
// @Success 200 {object} codersdk.ChatMessagesResponse
-// @Router /api/experimental/chats/{chat}/messages [get]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat}/messages [get]
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) getChatMessages(rw http.ResponseWriter, r *http.Request) {
@@ -1672,8 +1657,6 @@ func (api *API) getChatMessages(rw http.ResponseWriter, r *http.Request) {
})
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Get chat cost
// @ID get-chat-cost
// @Security CoderSessionToken
@@ -1681,8 +1664,7 @@ func (api *API) getChatMessages(rw http.ResponseWriter, r *http.Request) {
// @Produce json
// @Param chat path string true "Chat ID" format(uuid)
// @Success 200 {object} codersdk.ChatCost
-// @Router /api/experimental/chats/{chat}/cost [get]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat}/cost [get]
// @Description
// @Description Cost covers the whole chat tree: the root chat plus every
// @Description subagent chat beneath it. Requesting cost for a subagent chat
@@ -1743,8 +1725,7 @@ func (api *API) getChatCost(rw http.ResponseWriter, r *http.Request) {
// @Param chat path string true "Chat ID" format(uuid)
// @Param limit query int false "Page size, 0 to 2000. 0 (the default) means the server-side default of 500."
// @Success 200 {object} codersdk.ChatPromptsResponse
-// @Router /api/experimental/chats/{chat}/prompts [get]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat}/prompts [get]
// @Description
// @Description Returns the user-authored prompts in a chat, newest first,
// @Description with each prompt's text parts concatenated in the order they
@@ -1858,8 +1839,6 @@ func (api *API) authorizeChatWorkspaceExec(
return workspace, true
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Watch chat workspace git state via WebSockets
// @ID watch-chat-workspace-git-state-via-websockets
// @Security CoderSessionToken
@@ -1867,8 +1846,7 @@ func (api *API) authorizeChatWorkspaceExec(
// @Produce json
// @Param chat path string true "Chat ID" format(uuid)
// @Success 200 {object} codersdk.WorkspaceAgentGitServerMessage
-// @Router /api/experimental/chats/{chat}/stream/git [get]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat}/stream/git [get]
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) watchChatGit(rw http.ResponseWriter, r *http.Request) {
@@ -2111,7 +2089,7 @@ func (api *API) watchChatDesktop(rw http.ResponseWriter, r *http.Request) {
return
}
- // No read limit — RFB framebuffer updates can be large.
+ // No read limit because RFB framebuffer updates can be large.
conn.SetReadLimit(-1)
ctx, cancel := context.WithCancel(ctx)
@@ -2178,8 +2156,7 @@ func (api *API) applyChatTitleUpdate(
// @Produce json
// @Param chat path string true "Chat ID" format(uuid)
// @Success 200 {object} codersdk.Chat
-// @Router /api/experimental/chats/{chat}/context [put]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat}/context [put]
func (api *API) refreshChatContext(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
chat := httpmw.ChatParam(r)
@@ -2235,8 +2212,7 @@ func (api *API) refreshChatContext(rw http.ResponseWriter, r *http.Request) {
// @Param chat path string true "Chat ID" format(uuid)
// @Param request body codersdk.UpdateChatRequest true "Update chat request"
// @Success 204
-// @Router /api/experimental/chats/{chat} [patch]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat} [patch]
func (api *API) patchChat(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
chat := httpmw.ChatParam(r)
@@ -2564,8 +2540,6 @@ func writeCommonChatMutationError(ctx context.Context, rw http.ResponseWriter, e
return true
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Send chat message
// @ID send-chat-message
// @Security CoderSessionToken
@@ -2575,8 +2549,7 @@ func writeCommonChatMutationError(ctx context.Context, rw http.ResponseWriter, e
// @Param chat path string true "Chat ID" format(uuid)
// @Param request body codersdk.CreateChatMessageRequest true "Create chat message request"
// @Success 200 {object} codersdk.CreateChatMessageResponse
-// @Router /api/experimental/chats/{chat}/messages [post]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat}/messages [post]
func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -2769,8 +2742,6 @@ func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, response)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Edit chat message
// @ID edit-chat-message
// @Security CoderSessionToken
@@ -2781,8 +2752,7 @@ func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) {
// @Param message path int true "Message ID"
// @Param request body codersdk.EditChatMessageRequest true "Edit chat message request"
// @Success 200 {object} codersdk.EditChatMessageResponse
-// @Router /api/experimental/chats/{chat}/messages/{message} [patch]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat}/messages/{message} [patch]
func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -2928,7 +2898,14 @@ func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, response)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Delete chat queued message
+// @ID delete-chat-queued-message
+// @Security CoderSessionToken
+// @Tags Chats
+// @Param chat path string true "Chat ID" format(uuid)
+// @Param queuedMessage path int true "Queued message ID"
+// @Success 204
+// @Router /api/v2/chats/{chat}/queue/{queuedMessage} [delete]
func (api *API) deleteChatQueuedMessage(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
chat := httpmw.ChatParam(r)
@@ -2981,7 +2958,15 @@ func (api *API) deleteChatQueuedMessage(rw http.ResponseWriter, r *http.Request)
rw.WriteHeader(http.StatusNoContent)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Promote chat queued message
+// @ID promote-chat-queued-message
+// @Security CoderSessionToken
+// @Tags Chats
+// @Param chat path string true "Chat ID" format(uuid)
+// @Param queuedMessage path int true "Queued message ID"
+// @Produce json
+// @Success 202 {object} codersdk.Response
+// @Router /api/v2/chats/{chat}/queue/{queuedMessage}/promote [post]
func (api *API) promoteChatQueuedMessage(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -3099,17 +3084,15 @@ func (api *API) markChatAsRead(ctx context.Context, chatID uuid.UUID) {
}
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Stream chat events via WebSockets
// @ID stream-chat-events-via-websockets
// @Security CoderSessionToken
// @Tags Chats
// @Produce json
// @Param chat path string true "Chat ID" format(uuid)
-// @Success 200 {object} codersdk.ChatStreamEvent
-// @Router /api/experimental/chats/{chat}/stream [get]
-// @Description Experimental: this endpoint is subject to change.
+// @Param after_id query int false "Skip snapshot messages with id at or before this cursor"
+// @Success 200 {array} codersdk.ChatStreamEvent
+// @Router /api/v2/chats/{chat}/stream [get]
func (api *API) streamChat(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
chat := httpmw.ChatParam(r)
@@ -3240,8 +3223,6 @@ func (api *API) streamChat(rw http.ResponseWriter, r *http.Request) {
}
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Interrupt chat
// @ID interrupt-chat
// @Security CoderSessionToken
@@ -3249,8 +3230,7 @@ func (api *API) streamChat(rw http.ResponseWriter, r *http.Request) {
// @Param chat path string true "Chat ID" format(uuid)
// @Produce json
// @Success 200 {object} codersdk.Chat
-// @Router /api/experimental/chats/{chat}/interrupt [post]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat}/interrupt [post]
func (api *API) interruptChat(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
chat := httpmw.ChatParam(r)
@@ -3291,8 +3271,6 @@ func (api *API) interruptChat(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Chat(chat, nil, nil))
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Compact chat
// @ID compact-chat
// @Security CoderSessionToken
@@ -3300,9 +3278,8 @@ func (api *API) interruptChat(rw http.ResponseWriter, r *http.Request) {
// @Param chat path string true "Chat ID" format(uuid)
// @Produce json
// @Success 200 {object} codersdk.Chat
-// @Router /api/experimental/chats/{chat}/compact [post]
+// @Router /api/v2/chats/{chat}/compact [post]
// @x-apidocgen {"skip": true}
-// @Description Experimental: this endpoint is subject to change.
// @Description Requests a manual context compaction on an idle or errored
// @Description chat, clearing any stored error. The compaction runs
// @Description asynchronously through the chat worker and bypasses the
@@ -3364,8 +3341,6 @@ func (api *API) compactChat(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Chat(updated, nil, nil))
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Reconcile invalid chat state
// @ID reconcile-invalid-chat-state
// @Security CoderSessionToken
@@ -3373,8 +3348,7 @@ func (api *API) compactChat(rw http.ResponseWriter, r *http.Request) {
// @Produce json
// @Param chat path string true "Chat ID" format(uuid)
// @Success 200 {object} codersdk.Chat
-// @Router /api/experimental/chats/{chat}/reconcile-invalid [post]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat}/reconcile-invalid [post]
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) reconcileInvalidChatState(rw http.ResponseWriter, r *http.Request) {
@@ -3416,8 +3390,6 @@ func (api *API) reconcileInvalidChatState(rw http.ResponseWriter, r *http.Reques
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Chat(updated, nil, nil))
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Propose chat title
// @ID propose-chat-title
// @Security CoderSessionToken
@@ -3425,8 +3397,7 @@ func (api *API) reconcileInvalidChatState(rw http.ResponseWriter, r *http.Reques
// @Produce json
// @Param chat path string true "Chat ID" format(uuid)
// @Success 200 {object} codersdk.ProposeChatTitleResponse
-// @Router /api/experimental/chats/{chat}/title/propose [post]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat}/title/propose [post]
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) proposeChatTitle(rw http.ResponseWriter, r *http.Request) {
@@ -3478,8 +3449,6 @@ func (api *API) proposeChatTitle(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ProposeChatTitleResponse{Title: title})
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Get chat diff contents
// @ID get-chat-diff-contents
// @Security CoderSessionToken
@@ -3487,8 +3456,7 @@ func (api *API) proposeChatTitle(rw http.ResponseWriter, r *http.Request) {
// @Produce json
// @Param chat path string true "Chat ID" format(uuid)
// @Success 200 {object} codersdk.ChatDiffContents
-// @Router /api/experimental/chats/{chat}/diff [get]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/{chat}/diff [get]
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) getChatDiffContents(rw http.ResponseWriter, r *http.Request) {
@@ -4096,7 +4064,7 @@ func (api *API) resolveChatGitAccessToken(
slog.F("user_id", userID),
slog.Error(refreshErr),
)
- // Fall through — the existing token may still work
+ // Fall through because the existing token may still work.
// (e.g. GitHub tokens with no expiry).
} else {
link = refreshed
@@ -4329,6 +4297,14 @@ func parseCompactionThresholdKey(key string) (uuid.UUID, error) {
return id, nil
}
+// @Summary Get chat system prompt
+// @ID get-chat-system-prompt
+// @Security CoderSessionToken
+// @Tags Chats
+// @Produce json
+// @Success 200 {object} codersdk.ChatSystemPromptResponse
+// @Router /api/v2/chats/config/system-prompt [get]
+//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getChatSystemPrompt(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -4356,6 +4332,14 @@ func (api *API) getChatSystemPrompt(rw http.ResponseWriter, r *http.Request) {
// holding it for a single upsert.
const chatInstructionSettingsLockTimeout = 5 * time.Second
+// @Summary Update chat system prompt
+// @ID update-chat-system-prompt
+// @Security CoderSessionToken
+// @Tags Chats
+// @Accept json
+// @Param request body codersdk.UpdateChatSystemPromptRequest true "Request body"
+// @Success 204
+// @Router /api/v2/chats/config/system-prompt [put]
func (api *API) putChatSystemPrompt(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -4474,7 +4458,13 @@ func (api *API) putChatSystemPrompt(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNoContent)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Get chat plan mode instructions
+// @ID get-chat-plan-mode-instructions
+// @Security CoderSessionToken
+// @Tags Chats
+// @Produce json
+// @Success 200 {object} codersdk.ChatPlanModeInstructionsResponse
+// @Router /api/v2/chats/config/plan-mode-instructions [get]
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getChatPlanModeInstructions(rw http.ResponseWriter, r *http.Request) {
@@ -4498,7 +4488,14 @@ func (api *API) getChatPlanModeInstructions(rw http.ResponseWriter, r *http.Requ
})
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Update chat plan mode instructions
+// @ID update-chat-plan-mode-instructions
+// @Security CoderSessionToken
+// @Tags Chats
+// @Accept json
+// @Param request body codersdk.UpdateChatPlanModeInstructionsRequest true "Request body"
+// @Success 204
+// @Router /api/v2/chats/config/plan-mode-instructions [put]
func (api *API) putChatPlanModeInstructions(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -4613,11 +4610,9 @@ func readChatModelOverrideContext(
// @Produce json
// @Param organization path string true "Organization name or ID"
// @Success 200 {object} codersdk.ChatModelOverridesResponse
-// @Router /api/experimental/organizations/{organization}/chats/model-overrides [get]
+// @Router /api/v2/organizations/{organization}/chats/model-overrides [get]
// @x-apidocgen {"skip": true}
//
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getOrganizationChatModelOverrides(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -4653,10 +4648,8 @@ func (api *API) getOrganizationChatModelOverrides(rw http.ResponseWriter, r *htt
// @Param context path string true "Override context" Enums(general,explore,title_generation,compaction,advisor)
// @Param request body codersdk.UpdateChatModelOverrideRequest true "Model override"
// @Success 200 {object} codersdk.ChatModelOverrideResponse
-// @Router /api/experimental/organizations/{organization}/chats/model-overrides/{context} [put]
+// @Router /api/v2/organizations/{organization}/chats/model-overrides/{context} [put]
// @x-apidocgen {"skip": true}
-//
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
func (api *API) putOrganizationChatModelOverride(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
organization := httpmw.OrganizationParam(r)
@@ -4776,7 +4769,13 @@ func readChatPersonalModelOverrideContext(
return "", false
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Get chat personal model override settings
+// @ID get-chat-personal-model-override-settings
+// @Security CoderSessionToken
+// @Tags Chats
+// @Produce json
+// @Success 200 {object} codersdk.ChatPersonalModelOverridesAdminSettings
+// @Router /api/v2/chats/config/personal-model-overrides [get]
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getChatPersonalModelOverridesAdminSettings(rw http.ResponseWriter, r *http.Request) {
@@ -4916,7 +4915,14 @@ func (api *API) auditedChatOperationalSettingWrite(
return nil
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Update chat personal model override settings
+// @ID update-chat-personal-model-override-settings
+// @Security CoderSessionToken
+// @Tags Chats
+// @Accept json
+// @Param request body codersdk.UpdateChatPersonalModelOverridesAdminSettingsRequest true "Request body"
+// @Success 204
+// @Router /api/v2/chats/config/personal-model-overrides [put]
func (api *API) putChatPersonalModelOverridesAdminSettings(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
aReq, commitAudit := api.initChatOperationalSettingsAudit(rw, r)
@@ -4954,11 +4960,9 @@ func (api *API) putChatPersonalModelOverridesAdminSettings(rw http.ResponseWrite
// @Param organization path string true "Organization name or ID"
// @Param user path string true "User name, ID, or me"
// @Success 200 {object} codersdk.UserChatPersonalModelOverridesResponse
-// @Router /api/experimental/organizations/{organization}/members/{user}/chats/model-overrides [get]
+// @Router /api/v2/organizations/{organization}/members/{user}/chats/model-overrides [get]
// @x-apidocgen {"skip": true}
//
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getUserChatPersonalModelOverrides(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -5035,10 +5039,8 @@ func (api *API) getUserChatPersonalModelOverrides(rw http.ResponseWriter, r *htt
// @Param context path string true "Override context" Enums(root,general,explore)
// @Param request body codersdk.UpdateUserChatPersonalModelOverrideRequest true "Personal model override"
// @Success 204
-// @Router /api/experimental/organizations/{organization}/members/{user}/chats/model-overrides/{context} [put]
+// @Router /api/v2/organizations/{organization}/members/{user}/chats/model-overrides/{context} [put]
// @x-apidocgen {"skip": true}
-//
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
func (api *API) putUserChatPersonalModelOverride(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -5235,7 +5237,13 @@ func (api *API) deploymentChatDebugLoggingEnabled() bool {
return api.DeploymentValues != nil && api.DeploymentValues.AI.Chat.DebugLoggingEnabled.Value()
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Get chat debug logging setting
+// @ID get-chat-debug-logging-setting
+// @Security CoderSessionToken
+// @Tags Chats
+// @Produce json
+// @Success 200 {object} codersdk.ChatDebugLoggingAdminSettings
+// @Router /api/v2/chats/config/debug-logging [get]
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getChatDebugLogging(rw http.ResponseWriter, r *http.Request) {
@@ -5259,7 +5267,14 @@ func (api *API) getChatDebugLogging(rw http.ResponseWriter, r *http.Request) {
})
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Update chat debug logging setting
+// @ID update-chat-debug-logging-setting
+// @Security CoderSessionToken
+// @Tags Chats
+// @Accept json
+// @Param request body codersdk.UpdateChatDebugLoggingAllowUsersRequest true "Request body"
+// @Success 204
+// @Router /api/v2/chats/config/debug-logging [put]
func (api *API) putChatDebugLogging(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
aReq, commitAudit := api.initChatOperationalSettingsAudit(rw, r)
@@ -5289,7 +5304,13 @@ func (api *API) putChatDebugLogging(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNoContent)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Get user chat debug logging setting
+// @ID get-user-chat-debug-logging-setting
+// @Security CoderSessionToken
+// @Tags Chats
+// @Produce json
+// @Success 200 {object} codersdk.UserChatDebugLoggingSettings
+// @Router /api/v2/chats/config/user-debug-logging [get]
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getUserChatDebugLogging(rw http.ResponseWriter, r *http.Request) {
@@ -5330,7 +5351,14 @@ func (api *API) getUserChatDebugLogging(rw http.ResponseWriter, r *http.Request)
})
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Update user chat debug logging setting
+// @ID update-user-chat-debug-logging-setting
+// @Security CoderSessionToken
+// @Tags Chats
+// @Accept json
+// @Param request body codersdk.UpdateUserChatDebugLoggingRequest true "Request body"
+// @Success 204
+// @Router /api/v2/chats/config/user-debug-logging [put]
func (api *API) putUserChatDebugLogging(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -5459,7 +5487,13 @@ func (api *API) putChatAdvisorConfig(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNoContent)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Get chat workspace time to live
+// @ID get-chat-workspace-time-to-live
+// @Security CoderSessionToken
+// @Tags Chats
+// @Produce json
+// @Success 200 {object} codersdk.ChatWorkspaceTTLResponse
+// @Router /api/v2/chats/config/workspace-ttl [get]
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getChatWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
@@ -5491,7 +5525,14 @@ func (api *API) getChatWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
})
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Update chat workspace time to live
+// @ID update-chat-workspace-time-to-live
+// @Security CoderSessionToken
+// @Tags Chats
+// @Accept json
+// @Param request body codersdk.UpdateChatWorkspaceTTLRequest true "Request body"
+// @Success 204
+// @Router /api/v2/chats/config/workspace-ttl [put]
func (api *API) putChatWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
aReq, commitAudit := api.initChatOperationalSettingsAudit(rw, r)
@@ -5557,7 +5598,7 @@ func (api *API) putChatWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
// @Tags Chats
// @Produce json
// @Success 200 {object} codersdk.ChatRetentionDaysResponse
-// @Router /api/experimental/chats/config/retention-days [get]
+// @Router /api/v2/chats/config/retention-days [get]
// @x-apidocgen {"skip": true}
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
@@ -5587,7 +5628,7 @@ const retentionDaysMaximum = 3650 // ~10 years
// @Accept json
// @Param request body codersdk.UpdateChatRetentionDaysRequest true "Request body"
// @Success 204
-// @Router /api/experimental/chats/config/retention-days [put]
+// @Router /api/v2/chats/config/retention-days [put]
// @x-apidocgen {"skip": true}
func (api *API) putChatRetentionDays(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -5627,6 +5668,14 @@ func (api *API) putChatRetentionDays(rw http.ResponseWriter, r *http.Request) {
// getChatDebugRetentionDays returns the deployment-wide chat debug run
// retention window. Any authenticated user can read it; writes require admin.
//
+// @Summary Get chat debug retention days
+// @ID get-chat-debug-retention-days
+// @Security CoderSessionToken
+// @Tags Chats
+// @Produce json
+// @Success 200 {object} codersdk.ChatDebugRetentionDaysResponse
+// @Router /api/v2/chats/config/debug-retention-days [get]
+//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getChatDebugRetentionDays(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -5649,6 +5698,15 @@ const chatDebugRetentionDaysMaximum = 3650 // ~10 years
// putChatDebugRetentionDays updates the deployment-wide chat debug run
// retention window. Admin-only.
+//
+// @Summary Update chat debug retention days
+// @ID update-chat-debug-retention-days
+// @Security CoderSessionToken
+// @Tags Chats
+// @Accept json
+// @Param request body codersdk.UpdateChatDebugRetentionDaysRequest true "Request body"
+// @Success 204
+// @Router /api/v2/chats/config/debug-retention-days [put]
func (api *API) putChatDebugRetentionDays(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
aReq, commitAudit := api.initChatOperationalSettingsAudit(rw, r)
@@ -5688,6 +5746,14 @@ func (api *API) putChatDebugRetentionDays(rw http.ResponseWriter, r *http.Reques
// window. Any authenticated user can read it (same as retention
// days); writes require admin.
//
+// @Summary Get chat auto archive days
+// @ID get-chat-auto-archive-days
+// @Security CoderSessionToken
+// @Tags Chats
+// @Produce json
+// @Success 200 {object} codersdk.ChatAutoArchiveDaysResponse
+// @Router /api/v2/chats/config/auto-archive-days [get]
+//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getChatAutoArchiveDays(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -5710,6 +5776,15 @@ const autoArchiveDaysMaximum = 3650 // ~10 years
// putChatAutoArchiveDays updates the deployment-wide auto-archive
// window. Admin-only; documented in docs/ai-coder/agents/chats-api.md.
+//
+// @Summary Update chat auto archive days
+// @ID update-chat-auto-archive-days
+// @Security CoderSessionToken
+// @Tags Chats
+// @Accept json
+// @Param request body codersdk.UpdateChatAutoArchiveDaysRequest true "Request body"
+// @Success 204
+// @Router /api/v2/chats/config/auto-archive-days [put]
func (api *API) putChatAutoArchiveDays(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
aReq, commitAudit := api.initChatOperationalSettingsAudit(rw, r)
@@ -5745,7 +5820,13 @@ func (api *API) putChatAutoArchiveDays(rw http.ResponseWriter, r *http.Request)
rw.WriteHeader(http.StatusNoContent)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Get user chat custom prompt
+// @ID get-user-chat-custom-prompt
+// @Security CoderSessionToken
+// @Tags Chats
+// @Produce json
+// @Success 200 {object} codersdk.UserChatCustomPrompt
+// @Router /api/v2/chats/config/user-prompt [get]
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getUserChatCustomPrompt(rw http.ResponseWriter, r *http.Request) {
@@ -5772,7 +5853,15 @@ func (api *API) getUserChatCustomPrompt(rw http.ResponseWriter, r *http.Request)
})
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Update user chat custom prompt
+// @ID update-user-chat-custom-prompt
+// @Security CoderSessionToken
+// @Tags Chats
+// @Accept json
+// @Param request body codersdk.UserChatCustomPrompt true "Request body"
+// @Produce json
+// @Success 200 {object} codersdk.UserChatCustomPrompt
+// @Router /api/v2/chats/config/user-prompt [put]
func (api *API) putUserChatCustomPrompt(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
@@ -5815,8 +5904,13 @@ func (api *API) putUserChatCustomPrompt(rw http.ResponseWriter, r *http.Request)
}
// @Summary Get user chat compaction thresholds
+// @ID get-user-chat-compaction-thresholds
+// @Security CoderSessionToken
+// @Tags Chats
+// @Produce json
+// @Success 200 {object} codersdk.UserChatCompactionThresholds
+// @Router /api/v2/chats/config/user-compaction-thresholds [get]
// @x-apidocgen {"skip": true}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
func (api *API) getUserChatCompactionThresholds(rw http.ResponseWriter, r *http.Request) {
@@ -5875,9 +5969,17 @@ func (api *API) getUserChatCompactionThresholds(rw http.ResponseWriter, r *http.
httpapi.Write(ctx, rw, http.StatusOK, resp)
}
-// @Summary Set user chat compaction threshold for a model config
+// @Summary Update user chat compaction threshold
+// @ID update-user-chat-compaction-threshold
+// @Security CoderSessionToken
+// @Tags Chats
+// @Param modelConfig path string true "Model config ID" format(uuid)
+// @Accept json
+// @Param request body codersdk.UpdateUserChatCompactionThresholdRequest true "Request body"
+// @Produce json
+// @Success 200 {object} codersdk.UserChatCompactionThreshold
+// @Router /api/v2/chats/config/user-compaction-thresholds/{modelConfig} [put]
// @x-apidocgen {"skip": true}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
func (api *API) putUserChatCompactionThreshold(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
@@ -5948,9 +6050,14 @@ func (api *API) putUserChatCompactionThreshold(rw http.ResponseWriter, r *http.R
})
}
-// @Summary Delete user chat compaction threshold for a model config
+// @Summary Delete user chat compaction threshold
+// @ID delete-user-chat-compaction-threshold
+// @Security CoderSessionToken
+// @Tags Chats
+// @Param modelConfig path string true "Model config ID" format(uuid)
+// @Success 204
+// @Router /api/v2/chats/config/user-compaction-thresholds/{modelConfig} [delete]
// @x-apidocgen {"skip": true}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
func (api *API) deleteUserChatCompactionThreshold(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
@@ -5976,8 +6083,6 @@ func (api *API) deleteUserChatCompactionThreshold(rw http.ResponseWriter, r *htt
rw.WriteHeader(http.StatusNoContent)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Upload chat file
// @ID upload-chat-file
// @Security CoderSessionToken
@@ -5985,10 +6090,12 @@ func (api *API) deleteUserChatCompactionThreshold(rw http.ResponseWriter, r *htt
// @Accept image/png,image/jpeg,image/gif,image/webp,text/plain,text/markdown,text/csv,application/json,application/pdf
// @Produce json
// @Param organization query string true "Organization ID" format(uuid)
+// @Param Content-Disposition header string true "Attachment disposition carrying the file name" example(attachment; filename="image.png")
+// @Param request body string true "Raw file binary data"
+// @x-apidocgen {"rawBodyFile": "image.png"}
// @Success 201 {object} codersdk.UploadChatFileResponse
// @Failure 413 {object} codersdk.Response "Request body exceeds 10 MiB"
-// @Router /api/experimental/chats/files [post]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/files [post]
func (api *API) postChatFile(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -6138,8 +6245,6 @@ func (c ChatFileDownloadClaims) Validate(expected jwt.Expected) error {
return c.RegisteredClaims.Validate(expected)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Create chat file download URL
// @ID create-chat-file-download-url
// @Security CoderSessionToken
@@ -6147,9 +6252,8 @@ func (c ChatFileDownloadClaims) Validate(expected jwt.Expected) error {
// @Produce json
// @Param file path string true "File ID" format(uuid)
// @Success 200 {object} codersdk.ChatFileDownloadURLResponse
-// @Router /api/experimental/chats/files/{file}/download-url [post]
+// @Router /api/v2/chats/files/{file}/download-url [post]
// @x-apidocgen {"skip": true}
-// @Description Experimental: this endpoint is subject to change.
func (api *API) postChatFileDownloadURL(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
fileID, err := uuid.Parse(chi.URLParam(r, "file"))
@@ -6194,6 +6298,7 @@ func (api *API) postChatFileDownloadURL(rw http.ResponseWriter, r *http.Request)
return
}
+ // TODO(CODAGT-922): flip to /api/v2 when experimental mounts are removed.
downloadURL := api.AccessURL.JoinPath("api", "experimental", "chats", "files", fileID.String(), "download")
downloadURL.RawQuery = url.Values{"token": {token}}.Encode()
digest := sha256.Sum256(chatFile.Data)
@@ -6207,18 +6312,15 @@ func (api *API) postChatFileDownloadURL(rw http.ResponseWriter, r *http.Request)
})
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Download chat file with signed token
-// @ID download-chat-file
+// @ID download-chat-file-with-signed-token
// @Tags Chats
// @Produce image/png,image/jpeg,image/gif,image/webp,text/plain,text/markdown,text/csv,application/json,application/pdf
// @Param file path string true "File ID" format(uuid)
// @Param token query string true "Signed download token"
// @Success 200
-// @Router /api/experimental/chats/files/{file}/download [get]
+// @Router /api/v2/chats/files/{file}/download [get]
// @x-apidocgen {"skip": true}
-// @Description Experimental: this endpoint is subject to change.
func (api *API) downloadChatFile(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
fileID, err := uuid.Parse(chi.URLParam(r, "file"))
@@ -6259,8 +6361,6 @@ func (api *API) downloadChatFile(rw http.ResponseWriter, r *http.Request) {
api.serveChatFile(ctx, rw, chatFile)
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Get chat file
// @ID get-chat-file
// @Security CoderSessionToken
@@ -6268,8 +6368,7 @@ func (api *API) downloadChatFile(rw http.ResponseWriter, r *http.Request) {
// @Produce image/png,image/jpeg,image/gif,image/webp,text/plain,text/markdown,text/csv,application/json,application/pdf
// @Param file path string true "File ID" format(uuid)
// @Success 200
-// @Router /api/experimental/chats/files/{file} [get]
-// @Description Experimental: this endpoint is subject to change.
+// @Router /api/v2/chats/files/{file} [get]
func (api *API) chatFileByID(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -6526,6 +6625,14 @@ func convertAIProviderSummary(provider database.AIProvider) codersdk.AIProviderS
}
}
+// @Summary List user AI provider key configurations
+// @ID list-user-ai-provider-key-configurations
+// @Security CoderSessionToken
+// @Tags Chats
+// @Param user path string true "User ID, username, or me"
+// @Produce json
+// @Success 200 {array} codersdk.UserAIProviderKeyConfig
+// @Router /api/v2/users/{user}/ai-provider-keys [get]
func (api *API) listUserAIProviderKeyConfigs(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
targetUser := httpmw.UserParam(r)
@@ -6588,6 +6695,17 @@ func (api *API) listUserAIProviderKeyConfigs(rw http.ResponseWriter, r *http.Req
httpapi.Write(ctx, rw, http.StatusOK, configs)
}
+// @Summary Update user AI provider key
+// @ID update-user-ai-provider-key
+// @Security CoderSessionToken
+// @Tags Chats
+// @Param user path string true "User ID, username, or me"
+// @Param aiProvider path string true "AI provider ID" format(uuid)
+// @Accept json
+// @Param request body codersdk.CreateUserAIProviderKeyRequest true "Request body"
+// @Produce json
+// @Success 200 {object} codersdk.UserAIProviderKeyConfig
+// @Router /api/v2/users/{user}/ai-provider-keys/{aiProvider} [put]
func (api *API) upsertUserAIProviderKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !api.DeploymentValues.AI.BridgeConfig.AllowBYOK.Value() {
@@ -6664,6 +6782,14 @@ func (api *API) upsertUserAIProviderKey(rw http.ResponseWriter, r *http.Request)
})
}
+// @Summary Delete user AI provider key
+// @ID delete-user-ai-provider-key
+// @Security CoderSessionToken
+// @Tags Chats
+// @Param user path string true "User ID, username, or me"
+// @Param aiProvider path string true "AI provider ID" format(uuid)
+// @Success 204
+// @Router /api/v2/users/{user}/ai-provider-keys/{aiProvider} [delete]
func (api *API) deleteUserAIProviderKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
targetUser := httpmw.UserParam(r)
@@ -6800,13 +6926,13 @@ func (api *API) listDefaultOrganizationChatModelConfigs(rw http.ResponseWriter,
}
// @Summary List AI models and provider descriptors in an organization
-// @ID list-ai-models-by-organization
+// @ID list-ai-models-and-provider-descriptors-in-an-organization
// @Security CoderSessionToken
// @Tags Chats
// @Produce json
// @Param organization path string true "Organization name or ID"
// @Success 200 {object} codersdk.OrganizationChatModelsResponse
-// @Router /api/experimental/organizations/{organization}/chats/models [get]
+// @Router /api/v2/organizations/{organization}/chats/models [get]
// @x-apidocgen {"skip": true}
func (api *API) listChatModelConfigsByOrganization(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -6957,15 +7083,16 @@ func chatModelConfigRBACObject(config database.ChatModelConfig) rbac.Object {
// getChatModelConfig returns one chat model config after the organization and
// model identities have been resolved by route middleware.
+//
// @Summary Get an AI model
-// @ID get-ai-model
+// @ID get-an-ai-model
// @Security CoderSessionToken
// @Tags Chats
// @Produce json
// @Param organization path string true "Organization name or ID"
-// @Param model path string true "Model ID"
+// @Param model path string true "Model ID" format(uuid)
// @Success 200 {object} codersdk.ChatModel
-// @Router /api/experimental/organizations/{organization}/chats/models/{model} [get]
+// @Router /api/v2/organizations/{organization}/chats/models/{model} [get]
// @x-apidocgen {"skip": true}
//
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
@@ -7061,7 +7188,7 @@ func (api *API) auditChatModelConfigTransitions(
}
// @Summary Create an AI model in an organization
-// @ID create-ai-model
+// @ID create-an-ai-model-in-an-organization
// @Security CoderSessionToken
// @Tags Chats
// @Accept json
@@ -7069,7 +7196,7 @@ func (api *API) auditChatModelConfigTransitions(
// @Param organization path string true "Organization name or ID"
// @Param request body codersdk.CreateChatModelRequest true "Model"
// @Success 201 {object} codersdk.ChatModel
-// @Router /api/experimental/organizations/{organization}/chats/models [post]
+// @Router /api/v2/organizations/{organization}/chats/models [post]
// @x-apidocgen {"skip": true}
func (api *API) createChatModelConfig(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -7278,16 +7405,16 @@ func (api *API) createChatModelConfig(rw http.ResponseWriter, r *http.Request) {
}
// @Summary Update an AI model
-// @ID update-ai-model
+// @ID update-an-ai-model
// @Security CoderSessionToken
// @Tags Chats
// @Accept json
// @Produce json
// @Param organization path string true "Organization name or ID"
-// @Param model path string true "Model ID"
+// @Param model path string true "Model ID" format(uuid)
// @Param request body codersdk.UpdateChatModelRequest true "Model updates"
// @Success 200 {object} codersdk.ChatModel
-// @Router /api/experimental/organizations/{organization}/chats/models/{model} [patch]
+// @Router /api/v2/organizations/{organization}/chats/models/{model} [patch]
// @x-apidocgen {"skip": true}
func (api *API) updateChatModelConfig(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -7534,13 +7661,13 @@ func (api *API) updateChatModelConfig(rw http.ResponseWriter, r *http.Request) {
}
// @Summary Delete an AI model
-// @ID delete-ai-model
+// @ID delete-an-ai-model
// @Security CoderSessionToken
// @Tags Chats
// @Param organization path string true "Organization name or ID"
-// @Param model path string true "Model ID"
+// @Param model path string true "Model ID" format(uuid)
// @Success 204
-// @Router /api/experimental/organizations/{organization}/chats/models/{model} [delete]
+// @Router /api/v2/organizations/{organization}/chats/models/{model} [delete]
// @x-apidocgen {"skip": true}
func (api *API) deleteChatModelConfig(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -7922,7 +8049,15 @@ func ChatProviderAPIKeysFromDeploymentValues(
return chatprovider.ProviderAPIKeys{}
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
+// @Summary Submit chat tool results
+// @ID submit-chat-tool-results
+// @Security CoderSessionToken
+// @Tags Chats
+// @Param chat path string true "Chat ID" format(uuid)
+// @Accept json
+// @Param request body codersdk.SubmitToolResultsRequest true "Request body"
+// @Success 204
+// @Router /api/v2/chats/{chat}/tool-results [post]
//
//nolint:revive // HTTP handler writes to ResponseWriter.
func (api *API) postChatToolResults(rw http.ResponseWriter, r *http.Request) {
@@ -8128,18 +8263,15 @@ func (api *API) getChatDebugRun(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.ChatDebugRunDetail(run, steps))
}
-// EXPERIMENTAL: this endpoint is experimental and is subject to change.
-//
// @Summary Stream chat parts via WebSockets
// @ID stream-chat-parts-via-websockets
// @Security CoderSessionToken
// @Tags Chats
// @Produce json
// @Param chat path string true "Chat ID" format(uuid)
-// @Success 200 {object} codersdk.ChatStreamEvent
-// @Router /api/experimental/chats/{chat}/stream/parts [get]
+// @Success 200 {array} codersdk.ChatStreamEvent
+// @Router /api/v2/chats/{chat}/stream/parts [get]
// @x-apidocgen {"skip": true}
-// @Description Experimental: this endpoint is subject to change.
func (api *API) streamChatParts(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
chat := httpmw.ChatParam(r)
diff --git a/coderd/exp_chats_acl.go b/coderd/exp_chats_acl.go
index 889cd933745..cf0cc8d0d57 100644
--- a/coderd/exp_chats_acl.go
+++ b/coderd/exp_chats_acl.go
@@ -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)
diff --git a/coderd/exp_chats_model_acl.go b/coderd/exp_chats_model_acl.go
index de1922d4179..6a63757d6de 100644
--- a/coderd/exp_chats_model_acl.go
+++ b/coderd/exp_chats_model_acl.go
@@ -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()
diff --git a/coderd/httpmw/cors.go b/coderd/httpmw/cors.go
index 218aab6609f..56d8956fc4a 100644
--- a/coderd/httpmw/cors.go
+++ b/coderd/httpmw/cors.go
@@ -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
diff --git a/coderd/httpmw/ratelimit.go b/coderd/httpmw/ratelimit.go
index 17af4be2421..a36be183daf 100644
--- a/coderd/httpmw/ratelimit.go
+++ b/coderd/httpmw/ratelimit.go
@@ -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.
//
diff --git a/coderd/httpmw/ratelimit_test.go b/coderd/httpmw/ratelimit_test.go
index c6122685f87..8acb2fbd446 100644
--- a/coderd/httpmw/ratelimit_test.go
+++ b/coderd/httpmw/ratelimit_test.go
@@ -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()
diff --git a/coderd/mcp.go b/coderd/mcp.go
index 7ba73157c54..a1b297bfd36 100644
--- a/coderd/mcp.go
+++ b/coderd/mcp.go
@@ -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)