Files
coder/codersdk/chats.go
T
Michael Suchacz b2c8798e0e fix: replay stateless OpenAI reasoning across chat turns (#30298)
OpenAI Responses chats default to `store=false`, and chatd only replayed
reasoning as an `item_reference`, which needs provider-side storage.
Reasoning was therefore dropped between tool steps and turns, so
reasoning models lost their chain of thought mid-task.

This pins coder/fantasy `coder_2_33` at 6ba8681, which includes fantasy
#63 (and #64, #65, and #66). #63 captures each completed reasoning item
(ID, `encrypted_content`, summary) and replays it in full regardless of
`store`. In chatd:

- Every Responses call requests `reasoning.encrypted_content`. The
`store` default and configured includes are unchanged, and Chat
Completions requests are untouched.
- Prompt rebuilds keep reasoning parts without visible text when they
carry finalized encrypted state, so reasoning survives durable history
and coderd restarts.
- The provider-switch sanitizer also strips OpenAI reasoning from
history produced by a different or unknown provider instance, since
reasoning IDs and encrypted content only resolve where they were issued.
- A compaction model override on a different provider instance, or with
a different model, gets a compaction prompt without the chat's OpenAI
reasoning.
- Each saved reasoning part records the provider instance, model, and
credential (the deployment's or the user's personal API key) that
produced it, and the sanitizer keeps it only when all three match the
current turn, because another model, or the organization behind another
credential, can reject the encrypted content. Switching models, adding
or removing a personal key, or editing a config's model or provider
drops the earlier reasoning; switching back restores it. The record is
never returned by the API. Reasoning saved before this change has no
record and follows its model config.
- If OpenAI rejects replayed reasoning it cannot decrypt
(`invalid_encrypted_content`), for example after the deployment's key is
replaced with one from another organization, the request is retried once
without reasoning, and the reasoning that step produces is saved with
the provider's next rejection epoch. Epochs are kept separately for each
provider, model, and credential. Later steps and turns leave out
reasoning from earlier epochs, on any replica and after restarts, so
only the first request after the change is rejected and newer reasoning
keeps replaying. A retried step that returns no reasoning saves its
epoch on its first part. The advisor's prompt never includes OpenAI
reasoning, as on `main`.

## Testing

Rebased onto main at 98332c739b as 5735acb36ed (same tree as merging
the pre-rebase head a378071862a with main; older SHAs below refer to the
pre-rebase history). 3c17d807a33 then adds the model to the reasoning
record, 21156b2b32e pins Fantasy d55a867, which treats a response that
echoes `store: false` as unstored, bf795daa923 adds the retry and the
advisor change, 7eb8b1d029a moves the pin to 88cffb5 (`coder_2_33`
merged into #63; its OpenAI provider code is identical to d55a867), and
f4f40734a41 pins `coder_2_33` at 6ba8681 once #63 merged. That adds
fantasy #66, which replays the Responses message phase on assistant
text. abccede81f7 makes the retry remember rejected reasoning in the
process. The branch was then rebased onto main at abc2faed60, so every
SHA above refers to the pre-rebase history. 5fb3ce1689 replaces that
memory with the credential and a rejection epoch saved on each reasoning
part, and 62b5cb532a keeps epochs per provider, model, and credential
and saves the epoch when the retried step returns no reasoning.
1057beb327 hides the four internal fields from the Swagger spec and API
reference, and 29a705e099 merges main at d68b2c8b25. Its only
conflicts were the fantasy pin (kept at 6ba8681, whose `coder_2_33`
history includes main's 3510552) and test imports.

- Tests in `chatprompt`, `chatprovider`, `chatsanitize`, the
provider-switch sanitizer, and `integration_responses_test.go` cover
tool-step and durable replay, legacy and incomplete metadata, provider
switches, and a web search chat switching from `store=false` to
`store=true`. Integration tests also cover the retry, a later tool step
after it, the next turn on a fresh server against the same database
(with and without reasoning in the retried step), a personal key added
mid-chat, and the advisor prompt. Red-green verified, including a config
whose model is edited in place. The compaction override's model check
has unit tests and UAT coverage but no integration test. `go vet`,
targeted race tests, and the pre-commit hook (gen, fmt, lint, build)
pass. Local pre-push hooks were skipped; CI covers them.
- Remote UAT on dogfood with real gpt-5-mini and gpt-5.4 behind a
logging proxy that checks every request body. Rounds 1 and 2 failed on
dangling item references (both fixed). Rounds 3 to 5 passed
(ee3c0bf3d60, 0580870b5ff, a378071862a): tool-step and durable replay,
restarts, interrupts, provider switches and config repoints, web search
history, and compaction overrides. Both test providers shared one
upstream account, so cross-provider checks cover request shape, not a
real rejection. Round 6 at 21156b2b32e (model record) with gpt-5-mini
and gpt-5-nano: PASS. 151 Responses requests, all 200; no request
carried reasoning from a different model except an older, unrecorded
part that follows its config by design. Same-model config switches,
switching back, edits to a config's model, a mid-turn switch, restarts,
and a same-provider compaction override with a different model all
behaved as described. Round 7 at bf795daa923 (retry and advisor): PASS.
Through AI Gateway, a corrupted saved blob is rejected with HTTP 400
`invalid_encrypted_content`. All 14 rejected requests were retried once
without reasoning and returned 200, a different 400 was not retried, and
advisor requests on the chat model, a same-provider override, and
another provider carried no reasoning.
- f4f40734a41 changes only the pin. Its new OpenAI code is fantasy #66,
which UAT rounds 1 to 7 did not run. A live check of fantasy 6ba8681
through AI Gateway, two turns with tool calls on gpt-5.5 and gpt-5-mini
with `store` false and true: all 18 requests returned 200, and the
gpt-5.5 requests replayed encrypted reasoning followed by assistant
messages with the `commentary` and `final_answer` phases. `go test
./coderd/x/chatd/... ./codersdk/` passes (21 packages) on the same
fantasy tree.
- UAT round 8 at abccede81f7 (remembered rejection, and fantasy #66
through chatd) with gpt-5.5, gpt-5-mini, and gpt-5-nano: PASS. 110
requests; the 6 rejections were 5 corrupted saved blobs, each retried
once without reasoning and answered 200, and one unrelated `temperature`
error that was not retried. After the first rejection, later steps and
turns in that chat were never rejected again and replayed newer
reasoning. A coderd restart cost one more rejection, which also dropped
reasoning saved after the first one, and a second chat was unaffected.
gpt-5.5 requests replayed assistant messages with the phases the model
returned, with `store` false and true.
- UAT round 9 at 62b5cb532a (saved rejection epoch, personal key
switch) with gpt-5.5, gpt-5-mini, and gpt-5-nano: PASS. 158 Responses
and 7 Chat Completions requests; the 6 rejections were corrupted saved
blobs, each retried once without reasoning and answered 200, and an
unrelated `temperature` error was not retried. After a coderd restart,
and on a second coderd against the same database, the chat was not
rejected again and kept replaying the reasoning saved after the
rejection. Adding a personal key left out the deployment key's reasoning
before sending, and removing it brought that reasoning back. A retried
step without reasoning, two chats rejected at once, compaction, a model
switch after a rejection, and a switch to Chat Completions and back all
behaved as described.

## Review record

- Independent full-branch test audit at bbe1a1b27a: PASS.
- Codex through a378071862: compaction overrides kept foreign reasoning
and configs could be repointed to another provider (both fixed); a
request for documentation TODOs and two behaviors that already exist on
`main` (declined).
- Codex on 5735acb36e: two findings. Reasoning replayed after a
same-provider model switch: fixed in 3c17d807a33. A provider's base URL
edited in place keeps its reasoning: covered by the retry in
bf795daa923.
- Codex on 21156b2b32, bf795daa92, and 7eb8b1d029: no major issues.
Codex on f4f40734a4: rejected reasoning stayed in the chat's history, so
each later request in that chat was rejected and retried again without
any reasoning. Fixed in abccede81f7. Codex on abccede81f: that memory
was lost on a restart or another replica, where the next rejection also
dropped newer reasoning, and it grew without bound. Fixed in
5fb3ce1689. Codex on 5fb3ce1689: a rejection advanced the epoch for
every model and credential on the provider, and a retried step without
reasoning lost the new epoch. Both fixed in 62b5cb532a. Codex on
62b5cb532a: keep reasoning out of a config switched from Responses to
Chat Completions (declined: the Chat Completions converter already skips
reasoning parts, confirmed with a probe test). Codex on 29a705e099: no
major issues.
- ethanndickson: retry without reasoning on `invalid_encrypted_content`,
and keep OpenAI reasoning out of the advisor prompt. Both fixed in
bf795daa923. Then: in-process memory is not enough because dogfood
restarts on every merge and runs several replicas. Fixed in 5fb3ce1689
with his proposed rejection epoch in chat history, plus the credential
record, and tested with the next turn on a fresh server. Then: hide the
internal fields from Swagger, fixed in 1057beb327. Approved on
62b5cb532a.
- coder/fantasy#63 review: honoring an echoed `store: false` is fixed in
d55a867; replaying web searches that had no preceding reasoning is a
pre-existing gap, tracked in #30528.
- CI on 5735acb36ed: green after rerunning two failures outside this
change (a `cli` `TestSSH` data race, and a `test-go-pg` job timeout that
also fails on `main`). CI on 21156b2b32e: all checks pass. CI on
bf795daa923: all checks pass. CI on 7eb8b1d029a: all checks pass. CI on
f4f40734a41: all checks pass. CI on abccede81f7 did not run, because the
branch conflicted with `main` (#30492 changed the provider-switch
sanitizer to return database errors). CI on 62b5cb532a: all checks
pass. CI on 29a705e099: all checks pass.

> Xum, an AI coding agent, implemented, tested, and opened this PR on
behalf of @ibetitsmike.
2026-10-13 13:49:12 +02:00

3748 lines
160 KiB
Go

package codersdk
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"slices"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/invopop/jsonschema"
"golang.org/x/xerrors"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
)
// ChatCompactionThresholdKeyPrefix scopes per-model chat compaction
// threshold settings.
const ChatCompactionThresholdKeyPrefix = "chat_compaction_threshold_pct:"
// Defaults for the chat limits in [ChatConfig].
const (
// DefaultChatMaxStepsPerTurn is the default maximum number of steps in
// a chat turn.
DefaultChatMaxStepsPerTurn = 1200
// DefaultChatMaxGenerationRetries is the default maximum number of
// consecutive retries after a model generation fails with a transient
// error.
DefaultChatMaxGenerationRetries = 25
// DefaultChatMaxQueuedMessagesPerChat is the default maximum number of
// messages that can be queued in a chat.
DefaultChatMaxQueuedMessagesPerChat = 20
// DefaultChatMaxAttachmentsPerChat is the default maximum number of
// files linked to a chat.
DefaultChatMaxAttachmentsPerChat = 50
// DefaultChatMaxPromptBytes is the default maximum size in bytes of the
// deployment system prompt, the plan mode instructions, and each
// user's custom prompt.
DefaultChatMaxPromptBytes = 128 * 1024
// DefaultChatMaxConcurrentRecordingUploads is the default maximum
// number of virtual desktop recordings that each Coder server stores
// at the same time.
DefaultChatMaxConcurrentRecordingUploads = 25
)
// MaxChatFileSizeBytes is the upload-endpoint cap for chat
// attachments.
const MaxChatFileSizeBytes = 10 * 1024 * 1024
// Inline MCP server declaration caps. Clients can validate before sending.
const (
MaxInlineMCPServers = 5
MaxInlineMCPServersBytes = 24 * 1024
MaxInlineMCPServerSlugBytes = 32
MaxInlineMCPServerURLBytes = 2048
MaxInlineMCPServerHeaders = 16
MaxInlineMCPServerHeaderNameBytes = 128
MaxInlineMCPServerHeaderValueBytes = 8 * 1024
MaxInlineMCPServerToolFilters = 64
MaxInlineMCPServerToolNameBytes = 128
)
// AnthropicInlineImageCapBytes is Anthropic's documented per-image
// wire limit; the same cap applies to Bedrock-hosted Claude. Other
// providers have no documented per-image cap.
const AnthropicInlineImageCapBytes = 5 * 1024 * 1024
// ChatAttachmentMediaType is a media type that is allowed for durable
// chat file storage. The set is intentionally narrow; byte-level
// classification and inline-render rules live alongside the enforcement
// helpers in coderd/chatfiles.
type ChatAttachmentMediaType string
const (
ChatAttachmentMediaTypeApplicationJSON ChatAttachmentMediaType = "application/json"
ChatAttachmentMediaTypeApplicationPDF ChatAttachmentMediaType = "application/pdf"
ChatAttachmentMediaTypeImageGIF ChatAttachmentMediaType = "image/gif"
ChatAttachmentMediaTypeImageJPEG ChatAttachmentMediaType = "image/jpeg"
ChatAttachmentMediaTypeImagePNG ChatAttachmentMediaType = "image/png"
ChatAttachmentMediaTypeImageSVG ChatAttachmentMediaType = "image/svg+xml"
ChatAttachmentMediaTypeImageWEBP ChatAttachmentMediaType = "image/webp"
ChatAttachmentMediaTypeTextCSV ChatAttachmentMediaType = "text/csv"
ChatAttachmentMediaTypeTextMarkdown ChatAttachmentMediaType = "text/markdown"
ChatAttachmentMediaTypeTextPlain ChatAttachmentMediaType = "text/plain"
)
// AllChatAttachmentMediaTypes enumerates every durable chat attachment
// media type in the same lexical order the guts-generated TypeScript
// list uses, so the frontend file picker and the backend enforcement
// map stay in lockstep. Add new values in sorted order.
var AllChatAttachmentMediaTypes = []ChatAttachmentMediaType{
ChatAttachmentMediaTypeApplicationJSON,
ChatAttachmentMediaTypeApplicationPDF,
ChatAttachmentMediaTypeImageGIF,
ChatAttachmentMediaTypeImageJPEG,
ChatAttachmentMediaTypeImagePNG,
ChatAttachmentMediaTypeImageSVG,
ChatAttachmentMediaTypeImageWEBP,
ChatAttachmentMediaTypeTextCSV,
ChatAttachmentMediaTypeTextMarkdown,
ChatAttachmentMediaTypeTextPlain,
}
// CompactionThresholdKey returns the user-config key for a specific
// model configuration's compaction threshold.
func CompactionThresholdKey(modelID uuid.UUID) string {
return ChatCompactionThresholdKeyPrefix + modelID.String()
}
// ChatStatus represents the status of a chat.
type ChatStatus string
const (
ChatStatusWaiting ChatStatus = "waiting"
ChatStatusRunning ChatStatus = "running"
ChatStatusError ChatStatus = "error"
ChatStatusRequiresAction ChatStatus = "requires_action"
ChatStatusInterrupting ChatStatus = "interrupting"
)
// ChatClientType indicates whether a chat was created from the
// web UI or programmatically via the API.
type ChatClientType string
const (
ChatClientTypeUI ChatClientType = "ui"
ChatClientTypeAPI ChatClientType = "api"
)
// ChatTitleSource is where a chat's title came from.
type ChatTitleSource string
const (
// ChatTitleSourceFallback is derived from the first prompt, or is the
// default title of a chat created without one.
ChatTitleSourceFallback ChatTitleSource = "fallback"
// ChatTitleSourceGenerated is written by automatic title generation.
ChatTitleSourceGenerated ChatTitleSource = "generated"
// ChatTitleSourceUser is supplied by the caller at creation or by
// rename.
ChatTitleSourceUser ChatTitleSource = "user"
)
// Chat represents a chat session with an AI agent.
type Chat struct {
ID uuid.UUID `json:"id" format:"uuid"`
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
OwnerUsername string `json:"owner_username,omitempty"`
OwnerName string `json:"owner_name,omitempty"`
WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"`
ProjectID *uuid.UUID `json:"project_id,omitempty" format:"uuid"`
BuildID *uuid.UUID `json:"build_id,omitempty" format:"uuid"`
AgentID *uuid.UUID `json:"agent_id,omitempty" format:"uuid"`
ParentChatID *uuid.UUID `json:"parent_chat_id,omitempty" format:"uuid"`
RootChatID *uuid.UUID `json:"root_chat_id,omitempty" format:"uuid"`
LastModelConfigID uuid.UUID `json:"last_model_config_id" format:"uuid"`
LastReasoningEffort *string `json:"last_reasoning_effort,omitempty"`
Title string `json:"title"`
// TitleSource is where Title came from. A title write applies only when
// the current source ranks the same as or lower than the incoming one,
// in the order fallback, generated, user.
TitleSource ChatTitleSource `json:"title_source"`
// TitleUpdatedAt orders title changes. Title writes do not change
// UpdatedAt.
TitleUpdatedAt time.Time `json:"title_updated_at" format:"date-time"`
Status ChatStatus `json:"status"`
PlanMode ChatPlanMode `json:"plan_mode,omitempty"`
LastError *ChatError `json:"last_error,omitempty"`
LastTurnSummary *string `json:"last_turn_summary"`
// Summary is the persisted whole-chat summary, generated in the background.
// It is nil until the first summary has been produced.
Summary *string `json:"summary"`
// DiffStatus is the primary pull request. It is the ref with the
// most recent git report.
DiffStatus *ChatDiffStatus `json:"diff_status,omitempty"`
// DiffStatuses lists every ref the chat tracks. The order is
// stable and follows the first report of each ref. DiffStatus
// marks the primary.
DiffStatuses []ChatDiffStatus `json:"diff_statuses,omitempty"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
Archived bool `json:"archived"`
// Shared is true when this chat's root chat has explicit user or group ACL entries.
Shared bool `json:"shared"`
PinOrder int32 `json:"pin_order"`
MCPServerIDs []uuid.UUID `json:"mcp_server_ids" format:"uuid"`
Labels map[string]string `json:"labels"`
Files []ChatFileMetadata `json:"files,omitempty"`
// HasUnread is true when assistant messages exist beyond
// the owner's read cursor, which updates on stream
// connect and disconnect and via UpdateChatRequest.Read.
HasUnread bool `json:"has_unread"`
// Context reports the chat's pinned workspace-context state and
// whether it has drifted from the agent's latest pushed snapshot.
// Nil when the chat has no pinned context yet.
Context *ChatContext `json:"context,omitempty"`
// QueuedForCapacity reports that the chat is waiting for a concurrent
// agent slot. Single-chat reads derive it; list responses leave it false.
QueuedForCapacity bool `json:"queued_for_capacity,omitempty"`
Warnings []string `json:"warnings,omitempty"`
ClientType ChatClientType `json:"client_type"`
// InlineMCPServers lists the inline MCP servers declared on the chat,
// without headers. Only the single-chat GET sets it.
// Experimental.
InlineMCPServers []InlineMCPServer `json:"inline_mcp_servers,omitempty"`
// Children holds child (subagent) chats nested under this root
// chat. Always initialized to an empty slice so the JSON field
// is present as []. Child chats cannot create their own
// subagents, so nesting depth is capped at 1 and this slice is
// always empty for child chats.
Children []Chat `json:"children"`
}
// ChatProject groups related chats in an organization.
type ChatProject struct {
ID uuid.UUID `json:"id" format:"uuid"`
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
// Name is a display label and is not unique; ID identifies the project.
Name string `json:"name"`
Description string `json:"description"`
// Icon is a URL, typically an emoji image under /emojis, or empty for the
// default folder glyph.
Icon string `json:"icon"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
}
// CreateChatProjectRequest creates a chat project in the organization named
// by the route.
type CreateChatProjectRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description"`
Icon string `json:"icon,omitempty"`
}
// UpdateChatProjectRequest updates a chat project.
type UpdateChatProjectRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"`
}
// ChatProjectMemory is a durable memory shared by chats in a project.
type ChatProjectMemory struct {
ID uuid.UUID `json:"id" format:"uuid"`
ProjectID uuid.UUID `json:"project_id" format:"uuid"`
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
Name string `json:"name"`
Description string `json:"description"`
Body string `json:"body"`
CreatedBy uuid.UUID `json:"created_by" format:"uuid"`
CreatedByUsername string `json:"created_by_username"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
}
type CreateChatProjectMemoryRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"`
Body string `json:"body" validate:"required"`
}
// ChatContext reports a chat's pinned workspace context and whether it has
// drifted from the agent's latest pushed snapshot. The chat stays usable
// when dirty; refreshing re-pins it to the latest snapshot.
type ChatContext struct {
// Dirty is true when the agent's latest snapshot hash differs from the
// chat's pinned hash.
Dirty bool `json:"dirty"`
// DirtySince is when drift was first detected; nil when not dirty.
DirtySince *time.Time `json:"dirty_since,omitempty" format:"date-time"`
// Error is the snapshot-level error copied from the pinned snapshot
// (empty when healthy).
Error string `json:"error,omitempty"`
// Resources is the chat's pinned context (instruction files and
// skills) the prompt is built from, metadata only (no bodies). It is
// populated only on the single-chat GET response; list and watch
// payloads leave it nil to stay lightweight.
Resources []ChatContextResource `json:"resources,omitempty"`
}
// ChatContextResourceKind classifies a pinned context resource the prompt
// uses. Only the kinds that contribute to the prompt are reported.
type ChatContextResourceKind string
const (
ChatContextResourceKindInstructionFile ChatContextResourceKind = "instruction_file"
ChatContextResourceKindSkill ChatContextResourceKind = "skill"
ChatContextResourceKindMCPConfig ChatContextResourceKind = "mcp_config"
ChatContextResourceKindMCPServer ChatContextResourceKind = "mcp_server"
)
// ChatContextResource is one pinned workspace-context resource the chat's
// prompt is built from. It is metadata only; bodies are omitted. Reported
// only on the single-chat GET response.
type ChatContextResource struct {
// Source is the resource locator: the canonical file path for an
// instruction file, the skill directory for a skill, the file path for
// an MCP config, or the server name for an MCP server.
Source string `json:"source"`
Kind ChatContextResourceKind `json:"kind"`
// SizeBytes is the original payload size in bytes.
SizeBytes int64 `json:"size_bytes"`
// SkillName and SkillDescription are populated only for skill kinds.
SkillName string `json:"skill_name,omitempty"`
SkillDescription string `json:"skill_description,omitempty"`
// Tools lists the tools exposed by an MCP server. Populated only for the
// mcp_server kind; nil otherwise.
Tools []ChatContextTool `json:"tools,omitempty"`
// Status is the resource's health. Non-ok resources (invalid, unreadable,
// oversize, excluded) are still reported so the UI can surface why a
// resource was dropped from the prompt instead of silently omitting it;
// their body-specific fields (skill name, tools) are empty.
Status ChatContextResourceStatus `json:"status"`
// Error explains a non-ok Status; empty when healthy. May also carry a
// non-fatal warning when Status is ok.
Error string `json:"error,omitempty"`
}
// ChatContextResourceStatus is the health of a pinned context resource,
// mirroring the agent resolver's per-resource status.
type ChatContextResourceStatus string
const (
ChatContextResourceStatusOK ChatContextResourceStatus = "ok"
ChatContextResourceStatusOversize ChatContextResourceStatus = "oversize"
ChatContextResourceStatusUnreadable ChatContextResourceStatus = "unreadable"
ChatContextResourceStatusInvalid ChatContextResourceStatus = "invalid"
ChatContextResourceStatusExcluded ChatContextResourceStatus = "excluded"
)
// ChatContextTool is one tool exposed by a pinned MCP server, reported on the
// single-chat GET response. Metadata only; the input schema is omitted.
type ChatContextTool struct {
// Name is the tool name with the `<server>__` prefix the agent adds
// stripped, so it reads as the server exposes it.
Name string `json:"name"`
// Description is the tool's human-readable summary; may be empty.
Description string `json:"description,omitempty"`
}
// ChatFileMetadata contains lightweight metadata about a file
// associated with a chat, excluding the file content itself.
type ChatFileMetadata struct {
ID uuid.UUID `json:"id" format:"uuid"`
OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
Name string `json:"name"`
MimeType string `json:"mime_type"`
SizeBytes int64 `json:"size_bytes"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
}
// ChatMessage represents a single message in a chat.
type ChatMessage struct {
ID int64 `json:"id"`
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
CreatedBy *uuid.UUID `json:"created_by,omitempty" format:"uuid"`
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
Role ChatMessageRole `json:"role"`
Content []ChatMessagePart `json:"content,omitempty"`
Usage *ChatMessageUsage `json:"usage,omitempty"`
// QueuedMessageID is the ID of the queued message this message was
// promoted from. It matches ChatQueuedMessage.ID in the response that
// queued the message. It is nil when the message was not promoted from
// the queue (edits create a new message without it) or when a server
// version that did not record the link created it.
QueuedMessageID *int64 `json:"queued_message_id,omitempty"`
}
// ChatMessageUsage contains token usage information for a chat message.
type ChatMessageUsage struct {
InputTokens *int64 `json:"input_tokens,omitempty"`
OutputTokens *int64 `json:"output_tokens,omitempty"`
TotalTokens *int64 `json:"total_tokens,omitempty"`
// ReasoningTokens counts reasoning, which output_tokens already
// includes, so do not add the two.
ReasoningTokens *int64 `json:"reasoning_tokens,omitempty"`
CacheCreationTokens *int64 `json:"cache_creation_tokens,omitempty"`
CacheReadTokens *int64 `json:"cache_read_tokens,omitempty"`
ContextLimit *int64 `json:"context_limit,omitempty"`
}
// ChatMessageRole represents the role of a chat message sender.
type ChatMessageRole string
// ChatMessageRole enums.
const (
ChatMessageRoleSystem ChatMessageRole = "system"
ChatMessageRoleUser ChatMessageRole = "user"
ChatMessageRoleAssistant ChatMessageRole = "assistant"
ChatMessageRoleTool ChatMessageRole = "tool"
)
// ChatMessagePartType represents a structured message part type.
type ChatMessagePartType string
const (
ChatMessagePartTypeText ChatMessagePartType = "text"
ChatMessagePartTypeReasoning ChatMessagePartType = "reasoning"
ChatMessagePartTypeToolCall ChatMessagePartType = "tool-call"
ChatMessagePartTypeToolResult ChatMessagePartType = "tool-result"
ChatMessagePartTypeSource ChatMessagePartType = "source"
ChatMessagePartTypeFile ChatMessagePartType = "file"
ChatMessagePartTypeFileReference ChatMessagePartType = "file-reference"
ChatMessagePartTypeContextFile ChatMessagePartType = "context-file"
ChatMessagePartTypeSkill ChatMessagePartType = "skill"
ChatMessagePartTypeWorkspaceFileReference ChatMessagePartType = "workspace-file-reference"
// ChatMessagePartTypeHookContext is model context injected into a user
// prompt by a lifecycle hook. It is included in model prompt assembly
// and stripped from every client-facing conversion; the server rejects
// it in client-submitted content.
ChatMessagePartTypeHookContext ChatMessagePartType = "hook-context"
// ChatMessagePartTypeHookNotice is a user-facing lifecycle hook notice,
// either attached to a prompt or in its own row. It is excluded from model
// prompts and rejected in client-submitted content.
ChatMessagePartTypeHookNotice ChatMessagePartType = "hook-notice"
)
// AllChatMessagePartTypes returns all known ChatMessagePartType values.
func AllChatMessagePartTypes() []ChatMessagePartType {
return []ChatMessagePartType{
ChatMessagePartTypeText,
ChatMessagePartTypeReasoning,
ChatMessagePartTypeToolCall,
ChatMessagePartTypeToolResult,
ChatMessagePartTypeSource,
ChatMessagePartTypeFile,
ChatMessagePartTypeFileReference,
ChatMessagePartTypeContextFile,
ChatMessagePartTypeSkill,
ChatMessagePartTypeWorkspaceFileReference,
ChatMessagePartTypeHookContext,
ChatMessagePartTypeHookNotice,
}
}
// ChatMessagePart is a structured chunk of a chat message.
//
// WARNING: This type is both an API wire type and a database
// persistence format. Its JSON layout is stored in the
// chat_messages.content column. Field additions, renames, type
// changes, and omitempty behavior all affect backward-compatible
// deserialization of stored rows. Treat changes to this struct
// with the same care as a database migration.
//
// The variants struct tag declares which discriminated-union
// variants include each field in the generated TypeScript. Bare
// name = required, ? suffix = optional. Fields without a variants
// tag are excluded from the generated union. See
// scripts/apitypings/main.go for the codegen that reads these.
//
// omitempty rules (enforced by TestChatMessagePartVariantTags):
// - If a field is required (no ? suffix) in ANY variant, it
// must NOT use omitempty. Go would silently drop zero values
// that TypeScript expects to always be present.
// - If a field is optional (? suffix) in ALL of its variants,
// it MUST use omitempty. Sending zero values for fields that
// the frontend does not expect adds noise to the wire format
// and wastes space in persisted chat_messages rows.
type ChatMessagePart struct {
Type ChatMessagePartType `json:"type"`
Text string `json:"text" variants:"text,reasoning,hook-notice"`
ToolCallID string `json:"tool_call_id,omitempty" variants:"tool-call?,tool-result?"`
ToolName string `json:"tool_name,omitempty" variants:"tool-call?,tool-result?"`
MCPServerConfigID uuid.NullUUID `json:"mcp_server_config_id,omitempty" format:"uuid" variants:"tool-call?,tool-result?"`
Args json.RawMessage `json:"args,omitempty" variants:"tool-call?"`
ArgsDelta string `json:"args_delta,omitempty" variants:"tool-call?"`
// ParsedCommands holds parsed programs from an execute tool call's
// shell command, one entry per simple command in source order. Each
// entry is [program] or [program, arg] where arg is the first non-flag
// positional argument. Program names are normalized to their base
// name (e.g. /usr/bin/go becomes go). Only populated when ToolName
// is "execute" and the command parses successfully; nil otherwise.
ParsedCommands [][]string `json:"parsed_commands,omitempty" variants:"tool-call?"`
Result json.RawMessage `json:"result,omitempty" variants:"tool-result?"`
ResultDelta string `json:"result_delta,omitempty" variants:"tool-result?"`
ReasoningDelta string `json:"reasoning_delta,omitempty" variants:"tool-result?"`
ResultReset bool `json:"result_reset,omitempty" variants:"tool-result?"`
IsError bool `json:"is_error,omitempty" variants:"tool-result?"`
IsMedia bool `json:"is_media,omitempty" variants:"tool-result?"`
SourceID string `json:"source_id,omitempty" variants:"source?"`
URL string `json:"url" variants:"source"`
Title string `json:"title,omitempty" variants:"source?"`
MediaType string `json:"media_type" variants:"file"`
Name string `json:"name,omitempty" variants:"file?"`
Data []byte `json:"data,omitempty" variants:"file?"`
FileID uuid.NullUUID `json:"file_id,omitempty" format:"uuid" variants:"file?"`
FileName string `json:"file_name" variants:"file-reference"`
StartLine int `json:"start_line" variants:"file-reference"`
EndLine int `json:"end_line" variants:"file-reference"`
// The code content from the diff that was commented on.
Content string `json:"content" variants:"file-reference"`
// ProviderMetadata holds provider-specific response metadata
// (e.g. Anthropic cache control hints) as raw JSON. Internal
// only: stripped by db2sdk before API responses.
ProviderMetadata json.RawMessage `json:"provider_metadata,omitempty" typescript:"-"`
// ProviderIdentity records the provider that produced a reasoning
// part, because a model config's provider can change after the
// message was written. Internal only: stripped before API responses.
ProviderIdentity string `json:"provider_identity,omitempty" typescript:"-" swaggerignore:"true"`
// ProviderModel records the model that produced a reasoning part,
// because a model config's model can change after the message was
// written. Internal only: stripped before API responses.
ProviderModel string `json:"provider_model,omitempty" typescript:"-" swaggerignore:"true"`
// ProviderCredential records whether a reasoning part was produced
// with the deployment's credential or the user's personal API key,
// because encrypted reasoning only decrypts for the organization
// behind that credential. Internal only: stripped before API responses.
ProviderCredential string `json:"provider_credential,omitempty" typescript:"-" swaggerignore:"true"`
// ReasoningEpoch records how many times the provider had rejected the
// chat's encrypted reasoning from this provider, model, and credential
// before this reasoning part was produced. A step that produced no
// reasoning after a rejection records its epoch, provider, model, and
// credential on its first part instead. Internal only: stripped before
// API responses.
ReasoningEpoch int `json:"reasoning_epoch,omitempty" typescript:"-" swaggerignore:"true"`
// ProviderExecuted indicates the tool call was executed by
// the provider (e.g. Anthropic computer use).
ProviderExecuted bool `json:"provider_executed,omitempty" variants:"tool-call?,tool-result?"`
// HookRewritten indicates that a lifecycle hook replaced model-proposed tool input.
HookRewritten bool `json:"hook_rewritten,omitempty" variants:"tool-call?"`
// Narration marks assistant text that the model labeled as commentary
// on its work rather than its answer. Only providers that label their
// output set it.
Narration bool `json:"narration,omitempty" variants:"text?"`
// CreatedAt is the timestamp this part carries. The semantics
// depend on the part type: for tool-call and tool-result parts
// it is the time the call was emitted or the result was
// produced (tool duration is the result's created_at minus the
// call's created_at); for reasoning parts it is the time
// reasoning started streaming.
CreatedAt *time.Time `json:"created_at,omitempty" format:"date-time" variants:"tool-call?,tool-result?,reasoning?"`
// CompletedAt is the time a reasoning part finished streaming,
// so reasoning duration can be computed as completed_at minus
// created_at. For interrupted reasoning, this is the
// interruption time. Absent when reasoning timestamp data was
// not recorded (e.g. messages persisted before this feature
// was added).
CompletedAt *time.Time `json:"completed_at,omitempty" format:"date-time" variants:"reasoning?"`
// ContextFilePath is the absolute path of a file loaded into
// the LLM context (e.g. an AGENTS.md instruction file).
ContextFilePath string `json:"context_file_path" variants:"context-file"`
// ContextFileContent holds the file content sent to the LLM.
// Internal only: stripped before API responses to keep
// payloads small. The backend reads it when building the
// prompt via partsToMessageParts.
ContextFileContent string `json:"context_file_content,omitempty" typescript:"-"`
// ContextFileTruncated indicates the file exceeded the 64KiB
// instruction file limit and was truncated.
ContextFileTruncated bool `json:"context_file_truncated,omitempty" variants:"context-file?"`
// ContextFileAgentID is the workspace agent that provided
// this context file. Used to detect when the agent changes
// (e.g. workspace rebuilt) so instruction files can be
// re-persisted with fresh content.
ContextFileAgentID uuid.NullUUID `json:"context_file_agent_id,omitempty" format:"uuid" variants:"context-file?"`
// ContextFileOS is the operating system of the workspace
// agent. Internal only: used during prompt expansion so
// the LLM knows the OS even on turns where InsertSystem
// is not called.
ContextFileOS string `json:"context_file_os,omitempty" typescript:"-"`
// ContextFileDirectory is the working directory of the
// workspace agent. Internal only: same purpose as
// ContextFileOS.
ContextFileDirectory string `json:"context_file_directory,omitempty" typescript:"-"`
// SkillName is the kebab-case name of a discovered skill
// from the workspace's .agents/skills/ directory.
SkillName string `json:"skill_name" variants:"skill"`
// SkillDescription is the short description from the skill's
// SKILL.md frontmatter.
SkillDescription string `json:"skill_description,omitempty" variants:"skill?"`
// SkillDir is the absolute path to the skill directory inside
// the workspace filesystem. Internal only: used by
// read_skill/read_skill_file tools to locate skill files.
SkillDir string `json:"skill_dir,omitempty" typescript:"-"`
// ContextFileSkillMetaFile is the basename of the skill
// meta file (e.g. "SKILL.md") at the time of persistence.
// Internal only: restored on subsequent turns so the
// read_skill tool uses the correct filename even when the
// agent configured a non-default value.
ContextFileSkillMetaFile string `json:"context_file_skill_meta_file,omitempty" typescript:"-"`
// WorkspaceFilePath is the absolute path of a workspace upload.
// The bytes live on the workspace filesystem; only metadata is
// persisted on the message.
WorkspaceFilePath string `json:"workspace_file_path" variants:"workspace-file-reference"`
// WorkspaceFileName is the sanitized basename of a workspace upload.
WorkspaceFileName string `json:"workspace_file_name" variants:"workspace-file-reference"`
// WorkspaceFileSize is the byte size of a workspace upload.
WorkspaceFileSize int64 `json:"workspace_file_size" variants:"workspace-file-reference"`
// WorkspaceFileMediaType is the best-effort declared MIME type.
WorkspaceFileMediaType string `json:"workspace_file_media_type,omitempty" variants:"workspace-file-reference?"`
// WorkspaceFileWorkspaceID identifies the workspace whose
// filesystem holds the uploaded bytes. References are only
// readable while the chat stays bound to that workspace.
WorkspaceFileWorkspaceID uuid.UUID `json:"workspace_file_workspace_id" format:"uuid" variants:"workspace-file-reference"`
}
// StripInternal removes internal-only fields that must not be
// sent to API clients. Call before publishing via REST or SSE.
//
// Note: ArgsDelta, ResultDelta, ReasoningDelta, and ResultReset are
// intentionally preserved. They are streaming-only fields consumed by the
// frontend via WebSocket message_part events. ArgsDelta is produced by
// chatloop; ResultDelta, ReasoningDelta, and ResultReset are produced by
// the advisor streaming callbacks.
func (p *ChatMessagePart) StripInternal() {
p.ProviderMetadata = nil
p.ProviderIdentity = ""
p.ProviderModel = ""
p.ProviderCredential = ""
p.ReasoningEpoch = 0
if p.FileID.Valid {
p.Data = nil
}
p.ContextFileContent = ""
p.ContextFileOS = ""
p.ContextFileDirectory = ""
p.SkillDir = ""
p.ContextFileSkillMetaFile = ""
}
// ChatMessageText builds a text chat message part.
func ChatMessageText(text string) ChatMessagePart {
return ChatMessagePart{Type: ChatMessagePartTypeText, Text: text}
}
// ChatMessageReasoning builds a reasoning chat message part.
func ChatMessageReasoning(text string) ChatMessagePart {
return ChatMessagePart{Type: ChatMessagePartTypeReasoning, Text: text}
}
// ChatMessageToolCall builds a tool-call chat message part.
func ChatMessageToolCall(toolCallID, toolName string, args json.RawMessage) ChatMessagePart {
return ChatMessagePart{
Type: ChatMessagePartTypeToolCall,
ToolCallID: toolCallID,
ToolName: toolName,
Args: args,
}
}
// ChatMessageToolResult builds a tool-result chat message part.
// The isMedia flag marks the result as carrying binary media content
// (e.g. a screenshot) so that round-trip reconstruction preserves
// the media type instead of sending raw base64 as text tokens.
func ChatMessageToolResult(toolCallID, toolName string, result json.RawMessage, isError bool, isMedia bool) ChatMessagePart {
return ChatMessagePart{
Type: ChatMessagePartTypeToolResult,
ToolCallID: toolCallID,
ToolName: toolName,
Result: result,
IsError: isError,
IsMedia: isMedia,
}
}
// ChatMessageFile builds a file chat message part.
func ChatMessageFile(fileID uuid.UUID, mediaType string, name string) ChatMessagePart {
return ChatMessagePart{
Type: ChatMessagePartTypeFile,
FileID: uuid.NullUUID{UUID: fileID, Valid: true},
MediaType: mediaType,
Name: name,
}
}
// ChatMessageFileReference builds a file-reference chat message part.
func ChatMessageFileReference(fileName string, startLine, endLine int, content string) ChatMessagePart {
return ChatMessagePart{
Type: ChatMessagePartTypeFileReference,
FileName: fileName,
StartLine: startLine,
EndLine: endLine,
Content: content,
}
}
// ChatMessageWorkspaceFileReference builds a workspace-file-reference
// chat message part. The bytes live on the filesystem of workspace
// workspaceID at path; only metadata is persisted on the message.
func ChatMessageWorkspaceFileReference(workspaceID uuid.UUID, path, name string, size int64, mediaType string) ChatMessagePart {
return ChatMessagePart{
Type: ChatMessagePartTypeWorkspaceFileReference,
WorkspaceFilePath: path,
WorkspaceFileName: name,
WorkspaceFileSize: size,
WorkspaceFileMediaType: mediaType,
WorkspaceFileWorkspaceID: workspaceID,
}
}
// ChatMessageSource builds a source chat message part.
func ChatMessageSource(sourceID, sourceURL, title string) ChatMessagePart {
return ChatMessagePart{
Type: ChatMessagePartTypeSource,
SourceID: sourceID,
URL: sourceURL,
Title: title,
}
}
// ChatInputPartType represents an input part type for user chat input.
type ChatInputPartType string
const (
ChatInputPartTypeText ChatInputPartType = "text"
ChatInputPartTypeFile ChatInputPartType = "file"
ChatInputPartTypeFileReference ChatInputPartType = "file-reference"
ChatInputPartTypeWorkspaceFileReference ChatInputPartType = "workspace-file-reference"
)
// ChatInputPart is a single user input part for creating a chat.
type ChatInputPart struct {
Type ChatInputPartType `json:"type"`
Text string `json:"text,omitempty"`
FileID uuid.UUID `json:"file_id,omitempty" format:"uuid"`
// The following fields are only set when Type is
// ChatInputPartTypeFileReference.
FileName string `json:"file_name,omitempty"`
StartLine int `json:"start_line,omitempty"`
EndLine int `json:"end_line,omitempty"`
// The code content from the diff that was commented on.
Content string `json:"content,omitempty"`
// The following fields are only set when Type is
// ChatInputPartTypeWorkspaceFileReference.
WorkspaceFilePath string `json:"workspace_file_path,omitempty"`
WorkspaceFileName string `json:"workspace_file_name,omitempty"`
WorkspaceFileSize int64 `json:"workspace_file_size,omitempty"`
WorkspaceFileMediaType string `json:"workspace_file_media_type,omitempty"`
// WorkspaceFileWorkspaceID is the workspace the file was uploaded
// to, as returned by the upload endpoint. It must match the chat's
// currently bound workspace.
WorkspaceFileWorkspaceID uuid.UUID `json:"workspace_file_workspace_id,omitempty" format:"uuid"`
}
// SubmitToolResultsRequest is the body for POST /chats/{id}/tool-results.
type SubmitToolResultsRequest struct {
Results []ToolResult `json:"results"`
}
// ToolResult is the client's response to a dynamic tool call.
type ToolResult struct {
ToolCallID string `json:"tool_call_id"`
Output json.RawMessage `json:"output"`
IsError bool `json:"is_error"`
}
// MaxChatTitleRunes is the longest title accepted at creation or rename,
// counted in Unicode code points after trimming.
const MaxChatTitleRunes = 200
// CreateChatRequest is the request to create a new chat.
type CreateChatRequest struct {
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
// OwnerID makes another user the chat owner. It defaults to the
// caller. The chat runs with the owner's credentials, so setting it
// requires site-wide authority over that user.
OwnerID *uuid.UUID `json:"owner_id,omitempty" format:"uuid"`
// Content is the initial user message. It is optional: when
// empty, the chat is created idle with no initial user message
// and generation starts with the first message POSTed to
// /chats/{chat}/messages.
Content []ChatInputPart `json:"content"`
// Title, when set, is stored as the user title and automatic title
// generation does not run. It is trimmed and must then be non-empty
// and at most 200 Unicode code points (MaxChatTitleRunes), else the
// request fails with 400. When omitted, the title is derived from the
// first prompt and may later be replaced by a generated title.
Title *string `json:"title,omitempty"`
SystemPrompt string `json:"system_prompt,omitempty"`
WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"`
ProjectID *uuid.UUID `json:"project_id,omitempty" format:"uuid"`
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
MCPServerIDs []uuid.UUID `json:"mcp_server_ids,omitempty" format:"uuid"`
Labels map[string]string `json:"labels,omitempty"`
// UnsafeDynamicTools declares client-executed tools that the
// LLM can invoke. This API is highly experimental and highly
// subject to change.
UnsafeDynamicTools []DynamicTool `json:"unsafe_dynamic_tools,omitempty"`
// InlineMCPServers declares MCP servers by value on this chat, next
// to the org-configured servers selected by MCPServerIDs. Experimental.
InlineMCPServers []InlineMCPServerRequest `json:"inline_mcp_servers,omitempty"`
PlanMode ChatPlanMode `json:"plan_mode,omitempty"`
ClientType ChatClientType `json:"client_type,omitempty"`
}
// InlineMCPServerRequest declares a streamable HTTP MCP server by value on
// one chat. Headers are never returned. Header values are encrypted at
// rest when database encryption is configured.
type InlineMCPServerRequest struct {
Slug string `json:"slug"`
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
ToolAllowList []string `json:"tool_allow_list,omitempty"`
ToolDenyList []string `json:"tool_deny_list,omitempty"`
AllowInSubagents bool `json:"allow_in_subagents,omitempty"`
ForwardCoderHeaders bool `json:"forward_coder_headers,omitempty"`
}
// InlineMCPServer is the redacted view of an inline MCP server.
type InlineMCPServer struct {
ID uuid.UUID `json:"id" format:"uuid"`
Slug string `json:"slug"`
// URL is empty unless the chat owner makes the request.
URL string `json:"url"`
HasCustomHeaders bool `json:"has_custom_headers"`
ToolAllowList []string `json:"tool_allow_list"`
ToolDenyList []string `json:"tool_deny_list"`
AllowInSubagents bool `json:"allow_in_subagents"`
ForwardCoderHeaders bool `json:"forward_coder_headers"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
}
// UpdateChatRequest is the request to update a chat.
type UpdateChatRequest struct {
// Title, when set, is stored as the user title even when its text is
// unchanged, so a generated title never replaces it afterwards. It is
// validated like CreateChatRequest.Title.
Title *string `json:"title,omitempty"`
Archived *bool `json:"archived,omitempty"`
WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"`
// PinOrder controls the chat's pinned state and position.
// - nil: no change to pin state.
// - 0: unpin the chat.
// - >0 (chat is unpinned): pin the chat, appending it to
// the end of the pinned list. The specific value is
// ignored; the server assigns the next available position.
// - >0 (chat is already pinned): move the chat to the
// requested position, shifting neighbors as needed. The
// value is clamped to [1, pinned_count].
PinOrder *int32 `json:"pin_order,omitempty"`
Labels *map[string]string `json:"labels,omitempty"`
// Read moves the owner's read cursor, which drives HasUnread.
// - nil: no change.
// - true: mark every existing message as read.
// - false: clear the cursor so the chat reads as unread again.
//
// The cursor is owner-scoped, so only the chat owner may set this.
// Opening a chat's stream marks it read, so marking the chat the
// owner is currently viewing as unread does not persist.
Read *bool `json:"read,omitempty"`
// PlanMode switches the chat's persistent plan mode.
// nil: no change, ptr to "plan": enable, ptr to "": clear.
PlanMode *ChatPlanMode `json:"plan_mode,omitempty"`
}
// ChatBusyBehavior controls what happens when a user sends a message
// while the chat is already processing.
type ChatBusyBehavior string
const (
// ChatBusyBehaviorQueue queues the message for processing after
// the current run finishes.
ChatBusyBehaviorQueue ChatBusyBehavior = "queue"
// ChatBusyBehaviorInterrupt queues the message and interrupts
// the active run. The partial assistant response is persisted
// before the queued message is promoted, preserving correct
// conversation order.
ChatBusyBehaviorInterrupt ChatBusyBehavior = "interrupt"
)
// ChatPlanMode represents the persistent plan mode state of a chat.
type ChatPlanMode string
const (
// ChatPlanModePlan activates plan mode for the chat.
ChatPlanModePlan ChatPlanMode = "plan"
)
// CreateChatMessageRequest is the request to add a message to a chat.
type CreateChatMessageRequest struct {
Content []ChatInputPart `json:"content"`
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
MCPServerIDs *[]uuid.UUID `json:"mcp_server_ids,omitempty" format:"uuid"`
// InlineMCPServers replaces the inline MCP servers.
// nil: no change, empty: remove all.
InlineMCPServers *[]InlineMCPServerRequest `json:"inline_mcp_servers,omitempty"`
BusyBehavior ChatBusyBehavior `json:"busy_behavior,omitempty" enums:"queue,interrupt"`
// PlanMode switches the chat's persistent plan mode.
// nil: no change, ptr to "plan": enable, ptr to "": clear.
PlanMode *ChatPlanMode `json:"plan_mode,omitempty"`
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
}
// EditChatMessageRequest is the request to edit a user message in a chat.
type EditChatMessageRequest struct {
Content []ChatInputPart `json:"content"`
// ModelConfigID, 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.
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.
type CreateChatMessageResponse struct {
Message *ChatMessage `json:"message,omitempty"`
// Messages contains all user-visible messages inserted by the send, in
// insertion order. A queued send on an errored chat may promote the
// previous queue head, so clients must upsert the full batch.
Messages []ChatMessage `json:"messages,omitempty"`
QueuedMessage *ChatQueuedMessage `json:"queued_message,omitempty"`
Queued bool `json:"queued"`
Warnings []string `json:"warnings,omitempty"`
}
// EditChatMessageResponse is the response from editing a message in a chat.
type EditChatMessageResponse struct {
Message ChatMessage `json:"message"`
// Messages holds every user-visible message inserted by the edit, in
// insertion order. Hook-generated suffix messages may follow Message,
// so clients must upsert the full batch.
Messages []ChatMessage `json:"messages,omitempty"`
// DeletedMessageIDs holds the IDs of previously visible messages the
// edit removed, including stale hook notices from the edited turn.
// Clients should drop them from local caches.
DeletedMessageIDs []int64 `json:"deleted_message_ids,omitempty"`
Warnings []string `json:"warnings,omitempty"`
}
// UploadChatFileResponse is the response from uploading a chat file.
type UploadChatFileResponse struct {
ID uuid.UUID `json:"id" format:"uuid"`
}
// ChatFileDownloadURLResponse contains a short-lived URL for downloading a chat file.
type ChatFileDownloadURLResponse struct {
URL string `json:"url" format:"uri"`
ExpiresAt time.Time `json:"expires_at" format:"date-time"`
SHA256 string `json:"sha256"`
SizeBytes int64 `json:"size_bytes"`
Name string `json:"name"`
MimeType string `json:"mime_type"`
}
// UploadChatWorkspaceFileResponse describes a file uploaded to a
// chat's workspace filesystem.
type UploadChatWorkspaceFileResponse struct {
// Path is the absolute path of the file on the workspace.
Path string `json:"path"`
// Name is the final basename of the uploaded file.
Name string `json:"name"`
// Size is the number of bytes written to the workspace.
Size int64 `json:"size"`
// MediaType is the client-declared content type for display.
MediaType string `json:"media_type"`
// WorkspaceID is the workspace whose filesystem received the
// bytes. Message parts referencing this upload must carry it.
WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"`
}
// ChatMessagesResponse contains the messages and queued messages for a chat.
type ChatMessagesResponse struct {
Messages []ChatMessage `json:"messages"`
QueuedMessages []ChatQueuedMessage `json:"queued_messages"`
HasMore bool `json:"has_more"`
// TurnStartID is the ID of the user prompt that starts the turn
// containing the page's oldest message. Omitted for after_id-only polls
// and when no prompt is at or before that message.
TurnStartID *int64 `json:"turn_start_id,omitempty"`
}
// ChatPrompt is a single user-authored prompt in a chat, returned by
// GET /api/v2/chats/{chat}/prompts. The text field contains
// the concatenated text payload of the underlying chat message; non-text
// parts (tool calls, files, attachments) are omitted by the server.
type ChatPrompt struct {
ID int64 `json:"id"`
Text string `json:"text"`
}
// ChatPromptsResponse is the payload of
// GET /api/v2/chats/{chat}/prompts. Prompts are returned
// newest first so the client can index directly into the slice for
// up/down arrow history cycling.
type ChatPromptsResponse struct {
Prompts []ChatPrompt `json:"prompts"`
}
// ChatModelProviderUnavailableReason explains why a provider cannot be used.
type ChatModelProviderUnavailableReason string
const (
ChatModelProviderUnavailableMissingAPIKey ChatModelProviderUnavailableReason = "missing_api_key"
ChatModelProviderUnavailableFetchFailed ChatModelProviderUnavailableReason = "fetch_failed"
// #nosec G101
ChatModelProviderUnavailableReasonUserAPIKeyRequired ChatModelProviderUnavailableReason = "user_api_key_required"
)
// ChatUnsupportedProvider is a configured provider the Agents harness cannot
// use.
type ChatUnsupportedProvider struct {
// Provider is the provider type, e.g. "copilot".
Provider string `json:"provider"`
DisplayName string `json:"display_name"`
}
// ChatSystemPromptResponse is the response body for the chat system prompt
// configuration endpoint.
type ChatSystemPromptResponse struct {
SystemPrompt string `json:"system_prompt"`
IncludeDefaultSystemPrompt bool `json:"include_default_system_prompt"`
DefaultSystemPrompt string `json:"default_system_prompt"`
}
// UpdateChatSystemPromptRequest is the request body for updating the chat
// system prompt configuration.
type UpdateChatSystemPromptRequest struct {
SystemPrompt string `json:"system_prompt"`
IncludeDefaultSystemPrompt *bool `json:"include_default_system_prompt,omitempty"`
}
// OrganizationChatSystemPromptResponse is the response body for the
// organization chat system prompt endpoint.
type OrganizationChatSystemPromptResponse struct {
SystemPrompt string `json:"system_prompt"`
}
// UpdateOrganizationChatSystemPromptRequest is the request body for updating
// an organization's chat system prompt.
type UpdateOrganizationChatSystemPromptRequest struct {
SystemPrompt string `json:"system_prompt"`
}
// ChatPlanModeInstructionsResponse is the response body for the
// plan mode instructions configuration endpoint.
type ChatPlanModeInstructionsResponse struct {
PlanModeInstructions string `json:"plan_mode_instructions"`
}
// UpdateChatPlanModeInstructionsRequest is the request body for
// updating the plan mode instructions configuration.
type UpdateChatPlanModeInstructionsRequest struct {
PlanModeInstructions string `json:"plan_mode_instructions"`
}
// ChatModelOverrideContext identifies which chat model override context an
// organization override applies to.
type ChatModelOverrideContext string
const (
ChatModelOverrideContextGeneral ChatModelOverrideContext = "general"
ChatModelOverrideContextExplore ChatModelOverrideContext = "explore"
ChatModelOverrideContextTitleGeneration ChatModelOverrideContext = "title_generation"
ChatModelOverrideContextCompaction ChatModelOverrideContext = "compaction"
ChatModelOverrideContextAdvisor ChatModelOverrideContext = "advisor"
)
// Valid reports whether the override context is one of the supported values.
func (c ChatModelOverrideContext) Valid() bool {
switch c {
case ChatModelOverrideContextGeneral,
ChatModelOverrideContextExplore,
ChatModelOverrideContextTitleGeneration,
ChatModelOverrideContextCompaction,
ChatModelOverrideContextAdvisor:
return true
default:
return false
}
}
// AllChatModelOverrideContexts returns all supported override contexts.
func AllChatModelOverrideContexts() []ChatModelOverrideContext {
return []ChatModelOverrideContext{
ChatModelOverrideContextGeneral,
ChatModelOverrideContextExplore,
ChatModelOverrideContextTitleGeneration,
ChatModelOverrideContextCompaction,
ChatModelOverrideContextAdvisor,
}
}
// ChatModelOverrideResponse is the response body for one chat model override.
type ChatModelOverrideResponse struct {
Context ChatModelOverrideContext `json:"context"`
ModelConfigID string `json:"model_config_id"`
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
}
// ChatModelOverridesResponse is the response body for organization chat model overrides.
type ChatModelOverridesResponse struct {
Overrides []ChatModelOverrideResponse `json:"overrides"`
}
// UpdateChatModelOverrideRequest is the request body for updating the chat
// model override configuration endpoint.
type UpdateChatModelOverrideRequest struct {
ModelConfigID string `json:"model_config_id"`
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
}
// ChatPersonalModelOverrideContext identifies which chat context the user
// personal model override applies to.
type ChatPersonalModelOverrideContext string
const (
ChatPersonalModelOverrideContextRoot ChatPersonalModelOverrideContext = "root"
ChatPersonalModelOverrideContextGeneral ChatPersonalModelOverrideContext = "general"
ChatPersonalModelOverrideContextExplore ChatPersonalModelOverrideContext = "explore"
)
// ChatPersonalModelOverrideMode identifies how a user personal model override
// should resolve the effective model.
type ChatPersonalModelOverrideMode string
const (
ChatPersonalModelOverrideModeDeploymentDefault ChatPersonalModelOverrideMode = "deployment_default"
ChatPersonalModelOverrideModeChatDefault ChatPersonalModelOverrideMode = "chat_default"
ChatPersonalModelOverrideModeModel ChatPersonalModelOverrideMode = "model"
)
// ChatPersonalModelOverride is a resolved user personal model override.
type ChatPersonalModelOverride struct {
Context ChatPersonalModelOverrideContext `json:"context"`
Mode ChatPersonalModelOverrideMode `json:"mode"`
ModelConfigID string `json:"model_config_id"`
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
IsSet bool `json:"is_set"`
}
// ChatPersonalModelOverrideDeploymentDefaults describes the deployment-level
// defaults used when a personal override selects deployment_default.
type ChatPersonalModelOverrideDeploymentDefaults struct {
General ChatModelOverrideResponse `json:"general"`
Explore ChatModelOverrideResponse `json:"explore"`
}
// UserChatPersonalModelOverridesResponse is the response body for user
// personal model override settings.
type UserChatPersonalModelOverridesResponse struct {
Enabled bool `json:"enabled"`
Root ChatPersonalModelOverride `json:"root"`
General ChatPersonalModelOverride `json:"general"`
Explore ChatPersonalModelOverride `json:"explore"`
DeploymentDefaults ChatPersonalModelOverrideDeploymentDefaults `json:"deployment_defaults"`
}
// UpdateUserChatPersonalModelOverrideRequest is the request body for updating
// a user personal model override.
type UpdateUserChatPersonalModelOverrideRequest struct {
Mode ChatPersonalModelOverrideMode `json:"mode"`
ModelConfigID string `json:"model_config_id"`
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
}
// ChatPersonalModelOverridesAdminSettings describes whether users may manage
// personal model override settings.
type ChatPersonalModelOverridesAdminSettings struct {
AllowUsers bool `json:"allow_users"`
}
// UpdateChatPersonalModelOverridesAdminSettingsRequest is the request body for
// updating personal model override admin settings.
type UpdateChatPersonalModelOverridesAdminSettingsRequest struct {
AllowUsers bool `json:"allow_users"`
}
// UserChatCustomPrompt is the request and response body for the
// user chat custom prompt configuration endpoint.
type UserChatCustomPrompt struct {
CustomPrompt string `json:"custom_prompt"`
}
// UserChatCompactionThreshold is a user's per-model chat compaction
// threshold override.
type UserChatCompactionThreshold struct {
ModelConfigID uuid.UUID `json:"model_config_id" format:"uuid"`
ThresholdPercent int32 `json:"threshold_percent"`
}
// UserChatCompactionThresholds wraps the user's per-model chat
// compaction threshold overrides.
type UserChatCompactionThresholds struct {
Thresholds []UserChatCompactionThreshold `json:"thresholds"`
}
// UpdateUserChatCompactionThresholdRequest sets a user's per-model
// chat compaction threshold override.
type UpdateUserChatCompactionThresholdRequest struct {
ThresholdPercent int32 `json:"threshold_percent" validate:"min=0,max=100"`
}
// AdvisorConfig is the deployment-wide runtime configuration for the
// experimental chat advisor.
//
// EXPERIMENTAL: this type is experimental and is subject to change.
type AdvisorConfig struct {
// Enabled reflects whether the chat-advisor experiment is active.
// The experiment flag is the sole gate; this field is read-only and
// always matches the experiment state regardless of the stored DB value.
Enabled bool `json:"enabled"`
// MaxUsesPerRun caps how many times the advisor can be invoked per
// chat run. 0 means unlimited.
MaxUsesPerRun int `json:"max_uses_per_run"`
// MaxOutputTokens caps the advisor model response tokens. 0 means
// use the runtime default.
MaxOutputTokens int64 `json:"max_output_tokens"`
}
// UpdateAdvisorConfigRequest is the request body for updating advisor runtime configuration.
type UpdateAdvisorConfigRequest struct {
MaxUsesPerRun int `json:"max_uses_per_run"`
MaxOutputTokens int64 `json:"max_output_tokens"`
// Deprecated: moved to the organization model override endpoint.
DeprecatedModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
// Deprecated: moved to the organization model override endpoint.
DeprecatedReasoningEffort *string `json:"reasoning_effort,omitempty"`
}
// ChatComputerUseProvider identifies the provider that backs computer use for
// the virtual desktop.
type ChatComputerUseProvider string
const (
ChatComputerUseProviderAnthropic ChatComputerUseProvider = "anthropic"
ChatComputerUseProviderOpenAI ChatComputerUseProvider = "openai"
)
// AllChatComputerUseProviders contains every ChatComputerUseProvider value.
var AllChatComputerUseProviders = []ChatComputerUseProvider{
ChatComputerUseProviderAnthropic,
ChatComputerUseProviderOpenAI,
}
// Valid reports whether p is a supported computer use provider.
func (p ChatComputerUseProvider) Valid() bool {
return slices.Contains(AllChatComputerUseProviders, p)
}
// ChatComputerUseProviderResponse is the response for getting the computer use
// provider setting.
type ChatComputerUseProviderResponse struct {
Provider ChatComputerUseProvider `json:"provider"`
}
// UpdateChatComputerUseProviderRequest is the request to update the computer use
// provider setting.
type UpdateChatComputerUseProviderRequest struct {
Provider ChatComputerUseProvider `json:"provider"`
}
// ChatDebugLoggingAdminSettings describes the runtime admin setting
// that allows users to opt into chat debug logging.
type ChatDebugLoggingAdminSettings struct {
AllowUsers bool `json:"allow_users"`
ForcedByDeployment bool `json:"forced_by_deployment"`
}
// UserChatDebugLoggingSettings describes whether debug logging is
// active for the current user and whether the user may control it.
type UserChatDebugLoggingSettings struct {
DebugLoggingEnabled bool `json:"debug_logging_enabled"`
UserToggleAllowed bool `json:"user_toggle_allowed"`
ForcedByDeployment bool `json:"forced_by_deployment"`
}
// UpdateChatDebugLoggingAllowUsersRequest is the admin request to
// toggle whether users may opt into chat debug logging.
type UpdateChatDebugLoggingAllowUsersRequest struct {
AllowUsers bool `json:"allow_users"`
}
// UpdateUserChatDebugLoggingRequest is the per-user request to
// opt into or out of chat debug logging.
type UpdateUserChatDebugLoggingRequest struct {
DebugLoggingEnabled bool `json:"debug_logging_enabled"`
}
// ChatDebugStatus enumerates the lifecycle states shared by debug
// runs and steps. These values must match the literals used in
// FinalizeStaleChatDebugRows and all insert/update callers.
type ChatDebugStatus string
const (
ChatDebugStatusInProgress ChatDebugStatus = "in_progress"
ChatDebugStatusCompleted ChatDebugStatus = "completed"
ChatDebugStatusError ChatDebugStatus = "error"
ChatDebugStatusInterrupted ChatDebugStatus = "interrupted"
)
// ChatDebugTerminalStatuses returns the statuses that represent a
// finished lifecycle. The SQL query FinalizeStaleChatDebugRows uses
// a NOT IN list that must match these exactly. A test in
// coderd/database asserts this alignment at CI time.
func ChatDebugTerminalStatuses() []ChatDebugStatus {
return []ChatDebugStatus{
ChatDebugStatusCompleted,
ChatDebugStatusError,
ChatDebugStatusInterrupted,
}
}
// AllChatDebugStatuses contains every ChatDebugStatus value.
// Update this when adding new constants above.
var AllChatDebugStatuses = []ChatDebugStatus{
ChatDebugStatusInProgress,
ChatDebugStatusCompleted,
ChatDebugStatusError,
ChatDebugStatusInterrupted,
}
// ChatDebugRunKind labels the operation that produced the debug
// run. Each value corresponds to a distinct call-site in chatd.
type ChatDebugRunKind string
const (
ChatDebugRunKindChatTurn ChatDebugRunKind = "chat_turn"
ChatDebugRunKindTitleGeneration ChatDebugRunKind = "title_generation"
ChatDebugRunKindQuickgen ChatDebugRunKind = "quickgen"
ChatDebugRunKindCompaction ChatDebugRunKind = "compaction"
)
// AllChatDebugRunKinds contains every ChatDebugRunKind value.
// Update this when adding new constants above.
var AllChatDebugRunKinds = []ChatDebugRunKind{
ChatDebugRunKindChatTurn,
ChatDebugRunKindTitleGeneration,
ChatDebugRunKindQuickgen,
ChatDebugRunKindCompaction,
}
// ChatDebugStepOperation labels the model interaction type for a
// debug step.
type ChatDebugStepOperation string
const (
ChatDebugStepOperationStream ChatDebugStepOperation = "stream"
ChatDebugStepOperationGenerate ChatDebugStepOperation = "generate"
)
// AllChatDebugStepOperations contains every ChatDebugStepOperation
// value. Update this when adding new constants above.
var AllChatDebugStepOperations = []ChatDebugStepOperation{
ChatDebugStepOperationStream,
ChatDebugStepOperationGenerate,
}
// ChatDebugRunSummary is a lightweight run entry for list endpoints.
type ChatDebugRunSummary struct {
ID uuid.UUID `json:"id" format:"uuid"`
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
Kind ChatDebugRunKind `json:"kind"`
Status ChatDebugStatus `json:"status"`
Provider *string `json:"provider,omitempty"`
Model *string `json:"model,omitempty"`
Summary map[string]any `json:"summary"`
StartedAt time.Time `json:"started_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
FinishedAt *time.Time `json:"finished_at,omitempty" format:"date-time"`
}
// ChatDebugRun is the detailed run response returned by the run-detail
// endpoint. It includes the same summary fields as ChatDebugRunSummary
// along with the full step history for the run.
type ChatDebugRun struct {
ID uuid.UUID `json:"id" format:"uuid"`
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
RootChatID *uuid.UUID `json:"root_chat_id,omitempty" format:"uuid"`
ParentChatID *uuid.UUID `json:"parent_chat_id,omitempty" format:"uuid"`
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
TriggerMessageID *int64 `json:"trigger_message_id,omitempty"`
HistoryTipMessageID *int64 `json:"history_tip_message_id,omitempty"`
Kind ChatDebugRunKind `json:"kind"`
Status ChatDebugStatus `json:"status"`
Provider *string `json:"provider,omitempty"`
Model *string `json:"model,omitempty"`
Summary map[string]any `json:"summary"`
StartedAt time.Time `json:"started_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
FinishedAt *time.Time `json:"finished_at,omitempty" format:"date-time"`
Steps []ChatDebugStep `json:"steps"`
}
// ChatDebugStep is a single step within a debug run.
type ChatDebugStep struct {
ID uuid.UUID `json:"id" format:"uuid"`
RunID uuid.UUID `json:"run_id" format:"uuid"`
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
StepNumber int32 `json:"step_number"`
Operation ChatDebugStepOperation `json:"operation"`
Status ChatDebugStatus `json:"status"`
HistoryTipMessageID *int64 `json:"history_tip_message_id,omitempty"`
AssistantMessageID *int64 `json:"assistant_message_id,omitempty"`
NormalizedRequest map[string]any `json:"normalized_request"`
NormalizedResponse map[string]any `json:"normalized_response,omitempty"`
Usage map[string]any `json:"usage,omitempty"`
Attempts []map[string]any `json:"attempts"`
Error map[string]any `json:"error,omitempty"`
Metadata map[string]any `json:"metadata"`
StartedAt time.Time `json:"started_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
FinishedAt *time.Time `json:"finished_at,omitempty" format:"date-time"`
}
// DefaultChatWorkspaceTTL is the default TTL for chat workspaces.
// Zero means disabled; the template's own autostop setting applies.
const DefaultChatWorkspaceTTL = 0
// DefaultChatAutoArchiveDays is the default auto-archive window, in
// days, applied when no site config row exists. Zero disables
// auto-archival.
const DefaultChatAutoArchiveDays int32 = 0
// DefaultChatDebugRetentionDays is the default chat debug run retention
// window, in days, applied when no site config row exists. Set the
// config value to zero to disable the purge.
const DefaultChatDebugRetentionDays int32 = 30
// ChatWorkspaceTTLResponse is the response for getting the chat
// workspace TTL setting.
type ChatWorkspaceTTLResponse struct {
// WorkspaceTTLMillis is the workspace TTL in milliseconds.
// Zero means disabled; the template's own autostop setting applies.
WorkspaceTTLMillis int64 `json:"workspace_ttl_ms"`
}
// UpdateChatWorkspaceTTLRequest is the request to update the chat
// workspace TTL setting.
type UpdateChatWorkspaceTTLRequest struct {
// WorkspaceTTLMillis is the workspace TTL in milliseconds.
// Zero means disabled; the template's own autostop setting applies.
WorkspaceTTLMillis int64 `json:"workspace_ttl_ms"`
}
// ChatRetentionDaysResponse contains the current chat retention setting.
type ChatRetentionDaysResponse struct {
RetentionDays int32 `json:"retention_days"`
}
// UpdateChatRetentionDaysRequest is a request to update the chat
// retention period.
type UpdateChatRetentionDaysRequest struct {
RetentionDays int32 `json:"retention_days"`
}
// ChatDebugRetentionDaysResponse contains the current chat debug run
// retention setting.
type ChatDebugRetentionDaysResponse struct {
DebugRetentionDays int32 `json:"debug_retention_days"`
}
// UpdateChatDebugRetentionDaysRequest is a request to update the chat
// debug run retention period.
type UpdateChatDebugRetentionDaysRequest struct {
DebugRetentionDays int32 `json:"debug_retention_days"`
}
// ChatAutoArchiveDaysResponse contains the current chat auto-archive setting.
type ChatAutoArchiveDaysResponse struct {
AutoArchiveDays int32 `json:"auto_archive_days"`
}
// UpdateChatAutoArchiveDaysRequest is a request to update the chat
// auto-archive period.
type UpdateChatAutoArchiveDaysRequest struct {
AutoArchiveDays int32 `json:"auto_archive_days"`
}
// ParseChatWorkspaceTTL parses a stored TTL string, returning the
// default when the value is empty.
func ParseChatWorkspaceTTL(s string) (time.Duration, error) {
if s == "" {
return DefaultChatWorkspaceTTL, nil
}
d, err := time.ParseDuration(s)
if err != nil {
return 0, xerrors.Errorf("invalid duration %q: %w", s, err)
}
if d < 0 {
return 0, xerrors.New("duration must be non-negative")
}
return d, nil
}
// ChatProviderConfigSource describes how a provider entry is sourced.
type ChatProviderConfigSource string
const (
ChatProviderConfigSourceDatabase ChatProviderConfigSource = "database"
ChatProviderConfigSourceEnvPreset ChatProviderConfigSource = "env_preset"
ChatProviderConfigSourceSupported ChatProviderConfigSource = "supported"
)
// ChatProviderConfig is an admin-managed provider configuration.
type ChatProviderConfig struct {
ID uuid.UUID `json:"id" format:"uuid"`
Provider string `json:"provider"`
DisplayName string `json:"display_name"`
Icon string `json:"icon"`
Enabled bool `json:"enabled"`
HasAPIKey bool `json:"has_api_key"`
CentralAPIKeyEnabled bool `json:"central_api_key_enabled"`
AllowUserAPIKey bool `json:"allow_user_api_key"`
AllowCentralAPIKeyFallback bool `json:"allow_central_api_key_fallback"`
BaseURL string `json:"base_url,omitempty"`
Source ChatProviderConfigSource `json:"source"`
CreatedAt time.Time `json:"created_at,omitempty" format:"date-time"`
UpdatedAt time.Time `json:"updated_at,omitempty" format:"date-time"`
}
// AIProviderSummary is provider metadata embedded in other API responses.
type AIProviderSummary struct {
ID uuid.UUID `json:"id" format:"uuid"`
Type AIProviderType `json:"type"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
Icon string `json:"icon"`
Enabled bool `json:"enabled"`
Deleted bool `json:"deleted"`
}
// UserAIProviderKeyConfig is a provider summary from the current user's
// perspective. It reports key presence but never returns key material.
type UserAIProviderKeyConfig struct {
Provider AIProviderSummary `json:"provider"`
HasUserAPIKey bool `json:"has_user_api_key"`
HasProviderAPIKey bool `json:"has_provider_api_key"`
BYOKEnabled bool `json:"byok_enabled"`
}
// CreateUserAIProviderKeyRequest creates or replaces a user's API key
// for an AI provider.
type CreateUserAIProviderKeyRequest struct {
APIKey string `json:"api_key"`
}
// UserChatProviderConfig is a summary of a provider that allows
// user-supplied keys, as seen from the current user's perspective.
type UserChatProviderConfig struct {
ProviderID uuid.UUID `json:"provider_id" format:"uuid"`
Provider string `json:"provider"`
DisplayName string `json:"display_name"`
Icon string `json:"icon"`
Enabled bool `json:"enabled"`
HasUserAPIKey bool `json:"has_user_api_key"`
HasCentralAPIKeyFallback bool `json:"has_central_api_key_fallback"`
BYOKEnabled bool `json:"byok_enabled"`
}
// CreateUserChatProviderKeyRequest creates or replaces a user's API key
// for a provider.
type CreateUserChatProviderKeyRequest struct {
APIKey string `json:"api_key"`
}
// ChatModel is an org-scoped model configuration.
type ChatModel struct {
ID uuid.UUID `json:"id" format:"uuid"`
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
AIProviderID uuid.UUID `json:"ai_provider_id" format:"uuid"`
Model string `json:"model"`
DisplayName string `json:"display_name"`
Enabled bool `json:"enabled"`
IsDefault bool `json:"is_default"`
ContextLimit int64 `json:"context_limit"`
CompressionThreshold int32 `json:"compression_threshold"`
ModelConfig *ChatModelCallConfig `json:"model_config,omitempty"`
// ReasoningEfforts lists selectable reasoning effort values through
// the model's configured maximum.
ReasoningEfforts []string `json:"reasoning_efforts,omitempty"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
}
// ChatModelACL is the access control list for an organization-scoped chat
// model. Each principal includes the identity details needed to display and
// manage the ACL without separate directory lookups.
type ChatModelACL struct {
Users []ChatUser `json:"users"`
Groups []ChatGroup `json:"groups"`
}
// UpdateChatModelACLRequest is a sparse update of a chat model ACL. Only the
// listed principals change. ChatRoleDeleted removes an entry, while an omitted
// map or principal is unchanged.
type UpdateChatModelACLRequest struct {
UserRoles map[string]ChatRole `json:"user_roles,omitempty"`
GroupRoles map[string]ChatRole `json:"group_roles,omitempty"`
}
// ChatModelProviderOptions contains typed provider-specific options.
//
// Note: Azure models use the `openai` options shape.
// Note: Bedrock models use the `anthropic` options shape.
type ChatModelProviderOptions struct {
OpenAI *ChatModelOpenAIProviderOptions `json:"openai,omitempty"`
Anthropic *ChatModelAnthropicProviderOptions `json:"anthropic,omitempty"`
Google *ChatModelGoogleProviderOptions `json:"google,omitempty"`
OpenAICompat *ChatModelOpenAICompatProviderOptions `json:"openaicompat,omitempty"`
OpenRouter *ChatModelOpenRouterProviderOptions `json:"openrouter,omitempty"`
Vercel *ChatModelVercelProviderOptions `json:"vercel,omitempty"`
}
// ChatModelOpenAIProviderOptions configures OpenAI provider behavior.
type ChatModelOpenAIProviderOptions struct {
Include []string `json:"include,omitempty" description:"Model names to include in discovery" hidden:"true"`
Instructions *string `json:"instructions,omitempty" description:"System-level instructions prepended to the conversation" hidden:"true"`
LogitBias map[string]int64 `json:"logit_bias,omitempty" description:"Token IDs mapped to bias values from -100 to 100" hidden:"true"`
LogProbs *bool `json:"log_probs,omitempty" description:"Whether to return log probabilities of output tokens" hidden:"true"`
TopLogProbs *int64 `json:"top_log_probs,omitempty" description:"Number of most likely tokens to return log probabilities for" hidden:"true"`
MaxToolCalls *int64 `json:"max_tool_calls,omitempty" description:"Maximum number of tool calls per response"`
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty" description:"Whether the model may make multiple tool calls in parallel"`
User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"`
ReasoningSummary *string `json:"reasoning_summary,omitempty" description:"Controls whether reasoning tokens are summarized in the response" enum:"auto,concise,detailed"`
MaxCompletionTokens *int64 `json:"max_completion_tokens,omitempty" description:"Upper bound on tokens the model may generate"`
TextVerbosity *string `json:"text_verbosity,omitempty" description:"Controls the verbosity of the text response" enum:"low,medium,high"`
Prediction map[string]any `json:"prediction,omitempty" description:"Predicted output content to speed up responses" hidden:"true"`
Store *bool `json:"store,omitempty" description:"Whether to store the response on OpenAI for later retrieval via the API and dashboard logs"`
Metadata map[string]any `json:"metadata,omitempty" description:"Arbitrary metadata to attach to the request" hidden:"true"`
PromptCacheKey *string `json:"prompt_cache_key,omitempty" description:"Key for enabling cross-request prompt caching"`
SafetyIdentifier *string `json:"safety_identifier,omitempty" description:"Developer-specific safety identifier for the request" hidden:"true"`
ReasoningMode *string `json:"reasoning_mode,omitempty" providers:"openai" description:"Supported from the GPT-5.6 Sol generation of OpenAI models. Requests fail when Pro is set on a model that does not support it. Pro increases model work, latency, and token usage." enum:"pro"`
ServiceTier *string `json:"service_tier,omitempty" description:"Latency tier to use for processing the request" enum:"auto,default,flex,scale,priority"`
StructuredOutputs *bool `json:"structured_outputs,omitempty" description:"Whether to enable structured JSON output mode" hidden:"true"`
StrictJSONSchema *bool `json:"strict_json_schema,omitempty" description:"Whether to enforce strict adherence to the JSON schema" hidden:"true"`
WebSearchEnabled *bool `json:"web_search_enabled,omitempty" description:"Enable OpenAI web search tool for grounding responses with real-time information"`
SearchContextSize *string `json:"search_context_size,omitempty" description:"Amount of search context to use" enum:"low,medium,high" visible_when:"web_search_enabled"`
AllowedDomains []string `json:"allowed_domains,omitempty" label:"Web Search: Allowed Domains" description:"Restrict web search to these domains" visible_when:"web_search_enabled"`
}
// ChatModelAnthropicThinkingOptions configures Anthropic thinking budget.
type ChatModelAnthropicThinkingOptions struct {
BudgetTokens *int64 `json:"budget_tokens,omitempty" description:"Maximum number of tokens the model may use for thinking"`
}
// ChatModelAnthropicProviderOptions configures Anthropic provider behavior.
type ChatModelAnthropicProviderOptions struct {
SendReasoning *bool `json:"send_reasoning,omitempty" description:"Whether to include reasoning content in the response"`
Thinking *ChatModelAnthropicThinkingOptions `json:"thinking,omitempty" description:"Configuration for extended thinking"`
ThinkingDisplay *string `json:"thinking_display,omitempty" label:"Thinking Display" description:"Controls how Anthropic returns thinking content" enum:"summarized,omitted"`
DisableParallelToolUse *bool `json:"disable_parallel_tool_use,omitempty" description:"Whether to disable parallel tool execution"`
WebSearchEnabled *bool `json:"web_search_enabled,omitempty" description:"Enable Anthropic web search tool for grounding responses with real-time information"`
AllowedDomains []string `json:"allowed_domains,omitempty" label:"Web Search: Allowed Domains" description:"Restrict web search to these domains (cannot be used with blocked_domains)" visible_when:"web_search_enabled" conflicts_with:"blocked_domains"`
BlockedDomains []string `json:"blocked_domains,omitempty" label:"Web Search: Blocked Domains" description:"Block web search on these domains (cannot be used with allowed_domains)" visible_when:"web_search_enabled" conflicts_with:"allowed_domains"`
Context1MEnabled *bool `json:"context_1m_enabled,omitempty" label:"1M Context Window" description:"Send the anthropic-beta context-1m-2025-08-07 header to unlock the 1M token context window on supported Claude models. Pair with a matching Context Limit. Long-context pricing and higher latency may apply above 200K tokens."`
}
// ChatModelGoogleThinkingConfig configures Google thinking behavior.
type ChatModelGoogleThinkingConfig struct {
ThinkingBudget *int64 `json:"thinking_budget,omitempty" description:"Maximum number of tokens the model may use for thinking (cannot be used with thinking_level)" conflicts_with:"thinking_config.thinking_level"`
ThinkingLevel *string `json:"thinking_level,omitempty" description:"Thinking level for Gemini 3+ models, used when the user has not selected a reasoning effort (cannot be used with thinking_budget)" enum:"minimal,low,medium,high" conflicts_with:"thinking_config.thinking_budget"`
IncludeThoughts *bool `json:"include_thoughts,omitempty" description:"Whether to include thinking content in the response"`
}
// ChatModelGoogleSafetySetting configures Google safety filtering.
type ChatModelGoogleSafetySetting struct {
Category string `json:"category,omitempty" description:"The harm category to configure"`
Threshold string `json:"threshold,omitempty" description:"The blocking threshold for the harm category"`
}
// ChatModelGoogleProviderOptions configures Google provider behavior.
type ChatModelGoogleProviderOptions struct {
ThinkingConfig *ChatModelGoogleThinkingConfig `json:"thinking_config,omitempty" description:"Configuration for extended thinking"`
CachedContent string `json:"cached_content,omitempty" description:"Resource name of a cached content object" hidden:"true"`
SafetySettings []ChatModelGoogleSafetySetting `json:"safety_settings,omitempty" description:"Safety filtering settings for harmful content categories" hidden:"true"`
Threshold string `json:"threshold,omitempty" hidden:"true"`
WebSearchEnabled *bool `json:"web_search_enabled,omitempty" description:"Enable Google Search grounding for real-time information"`
}
// ChatModelOpenAICompatProviderOptions configures OpenAI-compatible behavior.
type ChatModelOpenAICompatProviderOptions struct {
User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"`
}
// ChatModelReasoningOptions configures reasoning behavior for model
// providers that support it.
type ChatModelReasoningOptions struct {
Enabled *bool `json:"enabled,omitempty" description:"Whether reasoning is enabled"`
Exclude *bool `json:"exclude,omitempty" description:"Whether to exclude reasoning content from the response"`
MaxTokens *int64 `json:"max_tokens,omitempty" description:"Maximum number of tokens for reasoning output"`
}
// ChatModelOpenRouterProvider configures OpenRouter routing preferences.
type ChatModelOpenRouterProvider struct {
Order []string `json:"order,omitempty" description:"Ordered list of preferred provider names"`
AllowFallbacks *bool `json:"allow_fallbacks,omitempty" description:"Whether to allow fallback to other providers"`
RequireParameters *bool `json:"require_parameters,omitempty" description:"Whether to require all parameters to be supported by the provider"`
DataCollection *string `json:"data_collection,omitempty" description:"Data collection policy preference"`
Only []string `json:"only,omitempty" description:"Restrict to only these provider names"`
Ignore []string `json:"ignore,omitempty" description:"Provider names to exclude from routing"`
Quantizations []string `json:"quantizations,omitempty" description:"Allowed model quantization levels"`
Sort *string `json:"sort,omitempty" description:"Sort order for provider selection"`
}
// ChatModelOpenRouterProviderOptions configures OpenRouter provider behavior.
type ChatModelOpenRouterProviderOptions struct {
Reasoning *ChatModelReasoningOptions `json:"reasoning,omitempty" description:"Configuration for reasoning behavior"`
ExtraBody map[string]any `json:"extra_body,omitempty" description:"Additional fields to include in the request body" hidden:"true"`
IncludeUsage *bool `json:"include_usage,omitempty" description:"Whether to include token usage information in the response" hidden:"true"`
LogitBias map[string]int64 `json:"logit_bias,omitempty" description:"Token IDs mapped to bias values from -100 to 100" hidden:"true"`
LogProbs *bool `json:"log_probs,omitempty" description:"Whether to return log probabilities of output tokens" hidden:"true"`
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty" description:"Whether the model may make multiple tool calls in parallel"`
User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"`
Provider *ChatModelOpenRouterProvider `json:"provider,omitempty" description:"Routing preferences for provider selection" hidden:"true"`
}
// ChatModelVercelGatewayProviderOptions configures Vercel routing behavior.
type ChatModelVercelGatewayProviderOptions struct {
Order []string `json:"order,omitempty" description:"Ordered list of preferred provider names"`
Models []string `json:"models,omitempty" description:"Model identifiers to route across"`
}
// ChatModelVercelProviderOptions configures Vercel provider behavior.
type ChatModelVercelProviderOptions struct {
Reasoning *ChatModelReasoningOptions `json:"reasoning,omitempty" description:"Configuration for reasoning behavior"`
ProviderOptions *ChatModelVercelGatewayProviderOptions `json:"providerOptions,omitempty" description:"Gateway routing options for provider selection" hidden:"true"`
User *string `json:"user,omitempty" description:"Unique identifier for the end user for abuse monitoring" hidden:"true"`
LogitBias map[string]int64 `json:"logit_bias,omitempty" description:"Token IDs mapped to bias values from -100 to 100" hidden:"true"`
LogProbs *bool `json:"logprobs,omitempty" description:"Whether to return log probabilities of output tokens" hidden:"true"`
TopLogProbs *int64 `json:"top_logprobs,omitempty" description:"Number of most likely tokens to return log probabilities for" hidden:"true"`
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty" description:"Whether the model may make multiple tool calls in parallel"`
ExtraBody map[string]any `json:"extra_body,omitempty" description:"Additional fields to include in the request body" hidden:"true"`
}
// Reasoning effort levels, ordered low to high for clamping and comparison.
const (
ChatModelReasoningEffortNone = "none"
ChatModelReasoningEffortMinimal = "minimal"
ChatModelReasoningEffortLow = "low"
ChatModelReasoningEffortMedium = "medium"
ChatModelReasoningEffortHigh = "high"
ChatModelReasoningEffortXHigh = "xhigh"
ChatModelReasoningEffortMax = "max"
)
var chatModelReasoningEffortValues = []string{
ChatModelReasoningEffortNone,
ChatModelReasoningEffortMinimal,
ChatModelReasoningEffortLow,
ChatModelReasoningEffortMedium,
ChatModelReasoningEffortHigh,
ChatModelReasoningEffortXHigh,
ChatModelReasoningEffortMax,
}
// ChatModelReasoningEffortValues returns the global reasoning effort scale.
func ChatModelReasoningEffortValues() []string {
return slices.Clone(chatModelReasoningEffortValues)
}
// ChatModelReasoningEffortConfig configures per-model reasoning effort
// bounds. When configured, Default and Max must both be provided before
// storing.
type ChatModelReasoningEffortConfig struct {
Default *string `json:"default,omitempty" label:"Default Reasoning Effort" description:"Reasoning effort used when the user has not selected one" enum:"none,minimal,low,medium,high,xhigh,max"`
Max *string `json:"max,omitempty" label:"Max Reasoning Effort" description:"Maximum reasoning effort the user may select" enum:"none,minimal,low,medium,high,xhigh,max"`
}
// ChatModelCallConfig configures per-call model behavior defaults.
type ChatModelCallConfig struct {
MaxOutputTokens *int64 `json:"max_output_tokens,omitempty" description:"Upper bound on tokens the model may generate"`
Temperature *float64 `json:"temperature,omitempty" description:"Sampling temperature between 0 and 2"`
TopP *float64 `json:"top_p,omitempty" description:"Nucleus sampling probability cutoff"`
TopK *int64 `json:"top_k,omitempty" description:"Number of highest-probability tokens to keep for sampling"`
PresencePenalty *float64 `json:"presence_penalty,omitempty" description:"Penalty for tokens that have already appeared in the output"`
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty" description:"Penalty for tokens based on their frequency in the output"`
ReasoningEffort *ChatModelReasoningEffortConfig `json:"reasoning_effort,omitempty" description:"Default and max reasoning effort for the model"`
OpenAIConfig *ChatModelOpenAIConfig `json:"openai_config,omitempty" description:"OpenAI client construction settings" providers:"openai"`
ProviderOptions *ChatModelProviderOptions `json:"provider_options,omitempty" description:"Provider-specific option overrides"`
}
// ChatModelOpenAIConfig holds settings applied once when the OpenAI client
// is built, not per request, including OpenAI-format models on Bedrock.
type ChatModelOpenAIConfig struct {
UseResponsesAPI *bool `json:"use_responses_api,omitempty" label:"Use Responses API" description:"Override which OpenAI API this model uses. Leave unset to decide from the provider SDK's known-model list, true to force the Responses API, false to force Chat Completions. Azure OpenAI providers ignore this and always follow the known-model list."`
ReasoningModel *bool `json:"reasoning_model,omitempty" label:"Reasoning Model" providers:"openai,bedrock" description:"Override whether this OpenAI-format model is treated as a reasoning model (reasoning effort and summary are sent, temperature and top_p are dropped). Leave unset to decide from the model name, true for models newer than the provider SDK knows, false to force plain sampling. Ignored for Anthropic models on Bedrock."`
}
// UnmarshalStrict rejects unknown fields except for removed pricing fields,
// which may still be present in stored model configuration JSON: the nested
// cost object and the four top-level per-million-token price keys that
// predate it (see migration 000435, which read both forms).
func (c *ChatModelCallConfig) UnmarshalStrict(data []byte) error {
type chatModelCallConfigAlias ChatModelCallConfig
aux := struct {
*chatModelCallConfigAlias
Cost json.RawMessage `json:"cost"`
InputPrice json.RawMessage `json:"input_price_per_million_tokens"`
OutputPrice json.RawMessage `json:"output_price_per_million_tokens"`
CacheReadPrice json.RawMessage `json:"cache_read_price_per_million_tokens"`
CacheWritePrice json.RawMessage `json:"cache_write_price_per_million_tokens"`
}{
chatModelCallConfigAlias: (*chatModelCallConfigAlias)(c),
}
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&aux); err != nil {
return err
}
if _, err := dec.Token(); !errors.Is(err, io.EOF) {
return xerrors.New("unexpected trailing data after JSON value")
}
return nil
}
// CreateChatModelRequest is the request body for an organization-scoped
// ChatModel. AIProviderID, Model, and a positive ContextLimit are required.
// Enabled defaults to true. IsDefault defaults to false when the organization
// already has a default model. The first model created in an organization is
// automatically promoted to default. CompressionThreshold defaults to 70, or
// 30 when ContextLimit is at least 500k tokens. An omitted ModelConfig uses the
// provider defaults.
type CreateChatModelRequest struct {
AIProviderID *uuid.UUID `json:"ai_provider_id,omitempty" format:"uuid"`
Model string `json:"model"`
DisplayName string `json:"display_name,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
IsDefault *bool `json:"is_default,omitempty"`
ContextLimit *int64 `json:"context_limit,omitempty"`
CompressionThreshold *int32 `json:"compression_threshold,omitempty"`
ModelConfig *ChatModelCallConfig `json:"model_config,omitempty"`
}
// UpdateChatModelRequest updates a ChatModel. Empty Model and DisplayName
// values preserve the stored values. Nil pointer fields preserve their stored
// values. This request cannot clear DisplayName.
type UpdateChatModelRequest struct {
AIProviderID *uuid.UUID `json:"ai_provider_id,omitempty" format:"uuid"`
Model string `json:"model,omitempty"`
DisplayName string `json:"display_name,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
IsDefault *bool `json:"is_default,omitempty"`
ContextLimit *int64 `json:"context_limit,omitempty"`
CompressionThreshold *int32 `json:"compression_threshold,omitempty"`
ModelConfig *ChatModelCallConfig `json:"model_config,omitempty"`
}
// ChatGitChange represents a git file change detected during a chat session.
type ChatGitChange struct {
ID uuid.UUID `json:"id" format:"uuid"`
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
FilePath string `json:"file_path"`
ChangeType string `json:"change_type"` // added, modified, deleted, renamed
OldPath *string `json:"old_path,omitempty"`
DiffSummary *string `json:"diff_summary,omitempty"`
DetectedAt time.Time `json:"detected_at" format:"date-time"`
}
// ChatDiffStatus represents cached diff status for a chat. The URL
// may point to a pull request or a branch page depending on whether
// a PR has been opened.
type ChatDiffStatus struct {
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
RemoteOrigin *string `json:"remote_origin,omitempty"`
GitBranch *string `json:"git_branch,omitempty"`
URL *string `json:"url,omitempty"`
PullRequestState *string `json:"pull_request_state,omitempty"`
PullRequestTitle string `json:"pull_request_title"`
PullRequestDraft bool `json:"pull_request_draft"`
ChangesRequested bool `json:"changes_requested"`
Additions int32 `json:"additions"`
Deletions int32 `json:"deletions"`
ChangedFiles int32 `json:"changed_files"`
AuthorLogin *string `json:"author_login,omitempty"`
AuthorAvatarURL *string `json:"author_avatar_url,omitempty"`
BaseBranch *string `json:"base_branch,omitempty"`
HeadBranch *string `json:"head_branch,omitempty"`
PRNumber *int32 `json:"pr_number,omitempty"`
Commits *int32 `json:"commits,omitempty"`
Approved *bool `json:"approved,omitempty"`
ReviewerCount *int32 `json:"reviewer_count,omitempty"`
RefreshedAt *time.Time `json:"refreshed_at,omitempty" format:"date-time"`
StaleAt *time.Time `json:"stale_at,omitempty" format:"date-time"`
}
// DiffStatusRef identifies one ref that a chat tracks. A chat has
// one diff status for each ref.
type DiffStatusRef struct {
RemoteOrigin string `json:"remote_origin"`
GitBranch string `json:"git_branch"`
}
// ChangedDiffStatus is the diff status of one ref after a change.
// When the ref has no stored status, Status has only chat_id.
type ChangedDiffStatus struct {
Ref DiffStatusRef `json:"ref"`
Status *ChatDiffStatus `json:"status"`
}
// ChatDiffContents represents the resolved diff text for a chat.
type ChatDiffContents struct {
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
Provider *string `json:"provider,omitempty"`
RemoteOrigin *string `json:"remote_origin,omitempty"`
Branch *string `json:"branch,omitempty"`
PullRequestURL *string `json:"pull_request_url,omitempty"`
Diff string `json:"diff,omitempty"`
}
// Chat git watch error messages. These are the user-visible messages
// the server returns in 400 responses from
// /api/v2/chats/{id}/stream/git when the chat cannot be
// observed through a workspace agent. They are exported so the CLI
// (and any future consumer) can match them structurally via
// IsChatGitWatchFallbackMessage instead of coupling to exact wording.
// Keep these in sync with coderd/exp_chats.go.
const (
ChatGitWatchNoWorkspaceMessage = "Chat has no workspace to watch."
ChatGitWatchWorkspaceNotFoundMessage = "Chat workspace not found."
ChatGitWatchNoEligibleAgentMessage = "No eligible agent found for chat workspace."
// ChatGitWatchAgentStatePrefix is the common prefix of the
// message produced by ChatGitWatchAgentStateMessage. The CLI
// uses it as a mechanical fingerprint for the "agent not yet
// connected" case without depending on the formatted values.
ChatGitWatchAgentStatePrefix = "Agent state is "
)
// ChatGitWatchAgentStateMessage is the user-visible error message
// returned from /api/v2/chats/{id}/stream/git when the
// chat workspace's agent is not in the connected state.
func ChatGitWatchAgentStateMessage(actual WorkspaceAgentStatus) string {
return fmt.Sprintf("%s%q, it must be in the %q state.", ChatGitWatchAgentStatePrefix, actual, WorkspaceAgentConnected)
}
// IsChatGitWatchFallbackMessage reports whether msg matches one of
// the 400-response messages /api/v2/chats/{id}/stream/git
// emits when the chat cannot be observed through a workspace agent.
// Clients should treat these cases as "no diff available" and fall
// back to the empty remote diff instead of surfacing a hard error.
func IsChatGitWatchFallbackMessage(msg string) bool {
trimmed := strings.TrimSpace(msg)
switch trimmed {
case ChatGitWatchNoWorkspaceMessage,
ChatGitWatchWorkspaceNotFoundMessage,
ChatGitWatchNoEligibleAgentMessage:
return true
}
return strings.HasPrefix(trimmed, ChatGitWatchAgentStatePrefix)
}
// ChatStreamEventType represents the kind of chat stream update.
type ChatStreamEventType string
const (
ChatStreamEventTypeMessagePart ChatStreamEventType = "message_part"
ChatStreamEventTypeMessage ChatStreamEventType = "message"
ChatStreamEventTypeStatus ChatStreamEventType = "status"
ChatStreamEventTypeError ChatStreamEventType = "error"
ChatStreamEventTypeQueueUpdate ChatStreamEventType = "queue_update"
ChatStreamEventTypeRetry ChatStreamEventType = "retry"
ChatStreamEventTypeActionRequired ChatStreamEventType = "action_required"
ChatStreamEventTypePreviewReset ChatStreamEventType = "preview_reset"
ChatStreamEventTypeHistoryReset ChatStreamEventType = "history_reset"
)
// ChatQueuedMessage represents a queued message waiting to be processed.
type ChatQueuedMessage struct {
ID int64 `json:"id"`
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
Content []ChatMessagePart `json:"content"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
}
// ChatStreamMessagePart is a streamed message part update.
type ChatStreamMessagePart struct {
Role ChatMessageRole `json:"role,omitempty"`
Part ChatMessagePart `json:"part"`
HistoryVersion int64 `json:"history_version,omitempty"`
GenerationAttempt int64 `json:"generation_attempt,omitempty"`
Seq int64 `json:"seq,omitempty"`
}
// ChatStreamStatus represents an updated chat status.
type ChatStreamStatus struct {
Status ChatStatus `json:"status"`
}
// ChatErrorKind classifies chat errors for consistent client rendering.
type ChatErrorKind string
const (
ChatErrorKindGeneric ChatErrorKind = "generic"
ChatErrorKindOverloaded ChatErrorKind = "overloaded"
ChatErrorKindRateLimit ChatErrorKind = "rate_limit"
ChatErrorKindTimeout ChatErrorKind = "timeout"
ChatErrorKindStreamSilenceTimeout ChatErrorKind = "stream_silence_timeout"
ChatErrorKindAuth ChatErrorKind = "auth"
ChatErrorKindConfig ChatErrorKind = "config"
ChatErrorKindUsageLimit ChatErrorKind = "usage_limit"
ChatErrorKindMissingKey ChatErrorKind = "missing_key"
ChatErrorKindProviderDisabled ChatErrorKind = "provider_disabled"
ChatErrorKindContentFilter ChatErrorKind = "content_filter"
ChatErrorKindHookDispatchFailed ChatErrorKind = "hook_dispatch_failed"
ChatErrorKindHookDenied ChatErrorKind = "hook_denied"
)
// AllChatErrorKinds contains every ChatErrorKind value.
// Update this when adding new constants above.
var AllChatErrorKinds = []ChatErrorKind{
ChatErrorKindGeneric,
ChatErrorKindOverloaded,
ChatErrorKindRateLimit,
ChatErrorKindTimeout,
ChatErrorKindStreamSilenceTimeout,
ChatErrorKindAuth,
ChatErrorKindConfig,
ChatErrorKindUsageLimit,
ChatErrorKindMissingKey,
ChatErrorKindProviderDisabled,
ChatErrorKindContentFilter,
ChatErrorKindHookDispatchFailed,
ChatErrorKindHookDenied,
}
// ChatError represents a terminal chat error in persisted chat state or the
// live stream.
type ChatError struct {
// Message is the normalized, user-facing error message.
Message string `json:"message"`
// Detail is optional provider-specific context shown alongside the
// normalized error message when available.
Detail string `json:"detail,omitempty"`
// Kind classifies the error for consistent client rendering.
Kind ChatErrorKind `json:"kind,omitempty"`
// Provider identifies the upstream model provider when known.
Provider string `json:"provider,omitempty"`
// Retryable reports whether the underlying error is transient.
Retryable bool `json:"retryable"`
// StatusCode is the best-effort upstream HTTP status code.
StatusCode int `json:"status_code,omitempty"`
}
// ChatStreamRetry represents an auto-retry status event in the stream.
// Published when the server automatically retries a failed LLM call.
type ChatStreamRetry struct {
// Attempt is the 1-indexed retry attempt number.
Attempt int `json:"attempt"`
// DelayMs is the backoff delay in milliseconds before the retry.
DelayMs int64 `json:"delay_ms"`
// Error is the normalized error message from the failed attempt.
Error string `json:"error"`
// Kind classifies the retry reason for consistent client rendering.
Kind ChatErrorKind `json:"kind,omitempty"`
// Provider identifies the upstream model provider when known.
Provider string `json:"provider,omitempty"`
// StatusCode is the best-effort upstream HTTP status code.
StatusCode int `json:"status_code,omitempty"`
// RetryingAt is the timestamp when the retry will be attempted.
RetryingAt time.Time `json:"retrying_at" format:"date-time"`
}
// ChatStreamActionRequired is the payload of an action_required stream event.
type ChatStreamActionRequired struct {
ToolCalls []ChatStreamToolCall `json:"tool_calls"`
}
// ChatStreamToolCall describes a pending dynamic tool call that the client
// must execute.
type ChatStreamToolCall struct {
ToolCallID string `json:"tool_call_id"`
ToolName string `json:"tool_name"`
Args string `json:"args"`
}
// DynamicToolCall represents a pending tool invocation from the
// chat stream that the client must execute and submit back.
type DynamicToolCall struct {
ToolCallID string `json:"tool_call_id"`
ToolName string `json:"tool_name"`
Args string `json:"args"`
}
// DynamicToolResponse holds the output of a dynamic tool
// execution. IsError indicates a tool-level error the LLM
// should see, as opposed to an infrastructure failure
// (returned as the error return value).
type DynamicToolResponse struct {
Content string `json:"content"`
IsError bool `json:"is_error"`
}
// DynamicTool describes a client-declared tool definition. On the
// client side, the Handler callback executes the tool when the LLM
// invokes it. On the server side, only Name, Description, and
// InputSchema are used (Handler is not serialized).
type DynamicTool struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
// InputSchema's JSON key "input_schema" uses snake_case for
// SDK consistency, deviating from the camelCase "inputSchema"
// convention used by MCP.
InputSchema json.RawMessage `json:"input_schema"`
// Handler executes the tool when the LLM invokes it.
// Not serialized; this only exists on the client side.
Handler func(ctx context.Context, call DynamicToolCall) (DynamicToolResponse, error) `json:"-"`
}
// NewDynamicTool creates a DynamicTool with a typed handler.
// The JSON schema is derived from T using invopop/jsonschema.
// The handler receives deserialized args and the DynamicToolCall metadata.
func NewDynamicTool[T any](
name, description string,
handler func(ctx context.Context, args T, call DynamicToolCall) (DynamicToolResponse, error),
) DynamicTool {
reflector := jsonschema.Reflector{
DoNotReference: true,
Anonymous: true,
AllowAdditionalProperties: true,
}
schema := reflector.Reflect(new(T))
schema.Version = ""
schemaJSON, err := json.Marshal(schema)
if err != nil {
panic(fmt.Sprintf("codersdk: failed to marshal schema for %q: %v", name, err))
}
return DynamicTool{
Name: name,
Description: description,
InputSchema: schemaJSON,
Handler: func(ctx context.Context, call DynamicToolCall) (DynamicToolResponse, error) {
var parsed T
if err := json.Unmarshal([]byte(call.Args), &parsed); err != nil {
return DynamicToolResponse{
Content: fmt.Sprintf("invalid parameters: %s", err),
IsError: true,
}, nil
}
return handler(ctx, parsed, call)
},
}
}
// ChatWatchEventKind represents the kind of event in the chat watch stream.
type ChatWatchEventKind string
const (
ChatWatchEventKindStatusChange ChatWatchEventKind = "status_change"
ChatWatchEventKindSummaryChange ChatWatchEventKind = "summary_change"
// ChatWatchEventKindChatSummaryChange carries the persisted whole-chat
// summary. It is distinct from SummaryChange (bound to last_turn_summary) so
// the frontend updates one field without disturbing the other.
ChatWatchEventKindChatSummaryChange ChatWatchEventKind = "chat_summary_change"
// ChatWatchEventKindTitleChange is published after each title write.
// Take only the title fields from it, ordered by title_updated_at,
// because a title write does not change updated_at.
ChatWatchEventKindTitleChange ChatWatchEventKind = "title_change"
ChatWatchEventKindCreated ChatWatchEventKind = "created"
ChatWatchEventKindDeleted ChatWatchEventKind = "deleted"
ChatWatchEventKindDiffStatusChange ChatWatchEventKind = "diff_status_change"
ChatWatchEventKindActionRequired ChatWatchEventKind = "action_required"
// ChatWatchEventKindContextDirty signals that the chat's pinned
// workspace context changed: it drifted from the agent's latest
// pushed snapshot, or hydration first populated it (a first-turn
// pin or an agent push reaching a not-yet-pinned chat). The chat
// stays usable; a refresh re-pins a drifted chat to the latest
// snapshot.
ChatWatchEventKindContextDirty ChatWatchEventKind = "context_dirty"
)
// ChatWatchEvent represents an event from the global chat watch stream.
// It delivers lifecycle events (created, status change, summary change,
// title change) for all of the authenticated user's chats. When Kind is
// ActionRequired, ToolCalls contains the pending dynamic tool
// invocations the client must execute and submit back.
type ChatWatchEvent struct {
Kind ChatWatchEventKind `json:"kind"`
Chat Chat `json:"chat"`
ToolCalls []ChatStreamToolCall `json:"tool_calls,omitempty"`
// ChangedDiffStatus is set when Kind is
// ChatWatchEventKindDiffStatusChange. It identifies the ref that
// changed.
ChangedDiffStatus *ChangedDiffStatus `json:"changed_diff_status,omitempty"`
}
// ChatStreamEvent represents a real-time update for chat streaming.
type ChatStreamEvent struct {
Type ChatStreamEventType `json:"type"`
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
Message *ChatMessage `json:"message,omitempty"`
MessagePart *ChatStreamMessagePart `json:"message_part,omitempty"`
Status *ChatStreamStatus `json:"status,omitempty"`
Error *ChatError `json:"error,omitempty"`
Retry *ChatStreamRetry `json:"retry,omitempty"`
QueuedMessages []ChatQueuedMessage `json:"queued_messages,omitempty"`
ActionRequired *ChatStreamActionRequired `json:"action_required,omitempty"`
}
// ChatCost is the AI Gateway cost for the requested chat's whole tree.
// Root and subagent chats report the same total.
// RequestCount counts every finished request in the tree, including ones that
// recorded no billable usage at all, such as a request that failed upstream.
// UnpricedRequestCount counts requests with at least one usage record whose
// model had no recorded price; RequestCount includes them and
// TotalCostMicros omits only their unpriced usage.
type ChatCost struct {
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
TotalCostMicros int64 `json:"total_cost_micros"`
RequestCount int64 `json:"request_count"`
UnpricedRequestCount int64 `json:"unpriced_request_count"`
}
// ChatHookDispatchFailedResponse is the error body returned when a
// lifecycle hook dispatch fails during a synchronous chat operation.
// Kind lets clients classify the failure without parsing message text.
type ChatHookDispatchFailedResponse struct {
Response
Kind ChatErrorKind `json:"kind"`
}
// ChatHookDeniedResponse is the error body returned when a lifecycle hook
// denies a synchronous chat operation. Kind lets clients classify the denial
// without parsing message text.
type ChatHookDeniedResponse struct {
Response
Kind ChatErrorKind `json:"kind"`
}
type ChatRole string
const (
ChatRoleRead ChatRole = "read"
ChatRoleDeleted ChatRole = ""
)
type ChatUser struct {
MinimalUser
Role ChatRole `json:"role" enums:"read"`
}
type ChatGroup struct {
Group
Role ChatRole `json:"role" enums:"read"`
}
type ChatACL struct {
Users []ChatUser `json:"users"`
Groups []ChatGroup `json:"groups"`
}
type UpdateChatACL struct {
UserRoles map[string]ChatRole `json:"user_roles,omitempty"`
GroupRoles map[string]ChatRole `json:"group_roles,omitempty"`
}
// ChatListSource controls which chats ListChats returns by ownership.
type ChatListSource string
const (
// ChatListSourceCreatedByMe returns chats owned by the caller.
ChatListSourceCreatedByMe ChatListSource = "created_by_me"
// ChatListSourceSharedWithMe returns chats shared with the caller.
ChatListSourceSharedWithMe ChatListSource = "shared_with_me"
)
// ListChatsOptions are optional parameters for ListChats.
type ListChatsOptions struct {
// Query supports raw chat search terms. If Query includes a source: term,
// Source must be empty.
Query string
// Source adds a source: term to Query.
Source ChatListSource
Labels map[string]string
ProjectID *uuid.UUID
Pagination
}
// ListChats returns all chats for the authenticated user.
func (c *Client) ListChats(ctx context.Context, opts *ListChatsOptions) ([]Chat, error) {
var reqOpts []RequestOption
if opts != nil {
reqOpts = append(reqOpts, opts.asRequestOption())
query := opts.Query
if opts.Source != "" {
if query != "" {
query += " "
}
query += "source:" + string(opts.Source)
}
if query != "" {
reqOpts = append(reqOpts, func(r *http.Request) {
q := r.URL.Query()
q.Set("q", query)
r.URL.RawQuery = q.Encode()
})
}
if opts.ProjectID != nil {
reqOpts = append(reqOpts, func(r *http.Request) {
q := r.URL.Query()
q.Set("project_id", opts.ProjectID.String())
r.URL.RawQuery = q.Encode()
})
}
if len(opts.Labels) > 0 {
reqOpts = append(reqOpts, func(r *http.Request) {
q := r.URL.Query()
for k, v := range opts.Labels {
q.Add("label", k+":"+v)
}
r.URL.RawQuery = q.Encode()
})
}
}
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats", nil, reqOpts...)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var chats []Chat
return chats, ReadBodyAsJSON(res, &chats)
}
func chatProjectsPath(organizationID uuid.UUID) string {
return fmt.Sprintf("/api/experimental/organizations/%s/chats/projects", organizationID)
}
func chatProjectPath(organizationID, projectID uuid.UUID) string {
return fmt.Sprintf("%s/%s", chatProjectsPath(organizationID), projectID)
}
// ListChatProjects lists the authenticated user's chat projects across all
// organizations.
func (c *ExperimentalClient) ListChatProjects(ctx context.Context) ([]ChatProject, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/projects", nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var projects []ChatProject
return projects, ReadBodyAsJSON(res, &projects)
}
// CreateChatProject creates a chat project in an organization.
func (c *ExperimentalClient) CreateChatProject(ctx context.Context, organizationID uuid.UUID, req CreateChatProjectRequest) (ChatProject, error) {
res, err := c.Request(ctx, http.MethodPost, chatProjectsPath(organizationID), req)
if err != nil {
return ChatProject{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return ChatProject{}, ReadBodyAsError(res)
}
var project ChatProject
return project, ReadBodyAsJSON(res, &project)
}
// GetChatProject gets a chat project.
func (c *ExperimentalClient) GetChatProject(ctx context.Context, organizationID, projectID uuid.UUID) (ChatProject, error) {
res, err := c.Request(ctx, http.MethodGet, chatProjectPath(organizationID, projectID), nil)
if err != nil {
return ChatProject{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatProject{}, ReadBodyAsError(res)
}
var project ChatProject
return project, ReadBodyAsJSON(res, &project)
}
// UpdateChatProject updates a chat project.
func (c *ExperimentalClient) UpdateChatProject(ctx context.Context, organizationID, projectID uuid.UUID, req UpdateChatProjectRequest) (ChatProject, error) {
res, err := c.Request(ctx, http.MethodPatch, chatProjectPath(organizationID, projectID), req)
if err != nil {
return ChatProject{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatProject{}, ReadBodyAsError(res)
}
var project ChatProject
return project, ReadBodyAsJSON(res, &project)
}
// DeleteChatProject deletes a chat project and detaches its chats.
func (c *ExperimentalClient) DeleteChatProject(ctx context.Context, organizationID, projectID uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, chatProjectPath(organizationID, projectID), nil)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// ListChatProjectMemories lists memories for a chat project.
func (c *ExperimentalClient) ListChatProjectMemories(ctx context.Context, organizationID, projectID uuid.UUID) ([]ChatProjectMemory, error) {
res, err := c.Request(ctx, http.MethodGet, chatProjectPath(organizationID, projectID)+"/memories", nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var memories []ChatProjectMemory
return memories, ReadBodyAsJSON(res, &memories)
}
// CreateChatProjectMemory creates a project memory.
func (c *ExperimentalClient) CreateChatProjectMemory(ctx context.Context, organizationID, projectID uuid.UUID, req CreateChatProjectMemoryRequest) (ChatProjectMemory, error) {
res, err := c.Request(ctx, http.MethodPost, chatProjectPath(organizationID, projectID)+"/memories", req)
if err != nil {
return ChatProjectMemory{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return ChatProjectMemory{}, ReadBodyAsError(res)
}
var memory ChatProjectMemory
return memory, ReadBodyAsJSON(res, &memory)
}
// GetChatProjectMemory gets a project memory.
func (c *ExperimentalClient) GetChatProjectMemory(ctx context.Context, organizationID, projectID, memoryID uuid.UUID) (ChatProjectMemory, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("%s/memories/%s", chatProjectPath(organizationID, projectID), memoryID), nil)
if err != nil {
return ChatProjectMemory{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatProjectMemory{}, ReadBodyAsError(res)
}
var memory ChatProjectMemory
return memory, ReadBodyAsJSON(res, &memory)
}
// DeleteChatProjectMemory deletes a project memory.
func (c *ExperimentalClient) DeleteChatProjectMemory(ctx context.Context, organizationID, projectID, memoryID uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("%s/memories/%s", chatProjectPath(organizationID, projectID), memoryID), nil)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// ListUserAIProviderKeyConfigs returns user-scoped AI provider key configs.
func (c *Client) ListUserAIProviderKeyConfigs(ctx context.Context, user string) ([]UserAIProviderKeyConfig, error) {
res, err := c.Request(ctx, http.MethodGet, userAIProviderKeysPath(user), nil)
if err != nil {
return nil, xerrors.Errorf("list user AI provider key configs: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var configs []UserAIProviderKeyConfig
return configs, ReadBodyAsJSON(res, &configs)
}
// UpsertUserAIProviderKey creates or replaces a user API key for an AI provider.
func (c *Client) UpsertUserAIProviderKey(ctx context.Context, user string, providerID uuid.UUID, req CreateUserAIProviderKeyRequest) (UserAIProviderKeyConfig, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("%s/%s", userAIProviderKeysPath(user), providerID), req)
if err != nil {
return UserAIProviderKeyConfig{}, xerrors.Errorf("upsert user AI provider key: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserAIProviderKeyConfig{}, ReadBodyAsError(res)
}
var config UserAIProviderKeyConfig
return config, ReadBodyAsJSON(res, &config)
}
// DeleteUserAIProviderKey deletes a user API key for an AI provider.
func (c *Client) DeleteUserAIProviderKey(ctx context.Context, user string, providerID uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("%s/%s", userAIProviderKeysPath(user), providerID), nil)
if err != nil {
return xerrors.Errorf("delete user AI provider key: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
func userAIProviderKeysPath(user string) string {
return fmt.Sprintf("/api/v2/users/%s/ai-provider-keys", url.PathEscape(user))
}
// ChatModels returns the chat model configs the caller can read in one
// organization, plus the redacted provider descriptors the authoring page
// needs, for org-scoped management and picker surfaces.
func (c *Client) ChatModels(ctx context.Context, organizationID uuid.UUID) (OrganizationChatModelsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/chats/models", organizationID), nil)
if err != nil {
return OrganizationChatModelsResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return OrganizationChatModelsResponse{}, ReadBodyAsError(res)
}
var resp OrganizationChatModelsResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// ChatModel fetches one chat model config by ID in an organization.
func (c *Client) ChatModel(ctx context.Context, organizationID, modelConfigID uuid.UUID) (ChatModel, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s", organizationID, modelConfigID), nil)
if err != nil {
return ChatModel{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatModel{}, ReadBodyAsError(res)
}
var config ChatModel
return config, ReadBodyAsJSON(res, &config)
}
// CreateChatModel creates a chat model config in the given organization.
func (c *Client) CreateChatModel(ctx context.Context, organizationID uuid.UUID, req CreateChatModelRequest) (ChatModel, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/chats/models", organizationID), req)
if err != nil {
return ChatModel{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return ChatModel{}, ReadBodyAsError(res)
}
var model ChatModel
return model, ReadBodyAsJSON(res, &model)
}
// UpdateChatModel updates a ChatModel in an organization.
func (c *Client) UpdateChatModel(ctx context.Context, organizationID, modelID uuid.UUID, req UpdateChatModelRequest) (ChatModel, error) {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s", organizationID, modelID), req)
if err != nil {
return ChatModel{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatModel{}, ReadBodyAsError(res)
}
var model ChatModel
return model, ReadBodyAsJSON(res, &model)
}
// ChatModelACL returns the access control list for a chat model in an
// organization.
func (c *Client) ChatModelACL(ctx context.Context, organizationID, modelID uuid.UUID) (ChatModelACL, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s/acl", organizationID, modelID), nil)
if err != nil {
return ChatModelACL{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatModelACL{}, ReadBodyAsError(res)
}
var modelACL ChatModelACL
return modelACL, ReadBodyAsJSON(res, &modelACL)
}
// ChatModelACLAvailable returns available users and groups that can be assigned
// chat model permissions. The optional request applies q/limit/offset/after_id
// to users. Groups reuse the user search query and q/limit semantics. Pass
// codersdk.UsersRequest{} when no filtering is desired.
func (c *Client) ChatModelACLAvailable(ctx context.Context, organizationID, modelID uuid.UUID, req UsersRequest) (ACLAvailable, error) {
res, err := c.Request(
ctx,
http.MethodGet,
fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s/acl/available", organizationID, modelID),
nil,
req.Pagination.asRequestOption(),
req.asRequestOption(),
)
if err != nil {
return ACLAvailable{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ACLAvailable{}, ReadBodyAsError(res)
}
var acl ACLAvailable
return acl, ReadBodyAsJSON(res, &acl)
}
// UpdateChatModelACL applies a sparse access control list update to a chat
// model in an organization.
func (c *Client) UpdateChatModelACL(ctx context.Context, organizationID, modelID uuid.UUID, req UpdateChatModelACLRequest) error {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s/acl", organizationID, modelID), req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// DeleteChatModel deletes a ChatModel in an organization.
func (c *Client) DeleteChatModel(ctx context.Context, organizationID, modelID uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/organizations/%s/chats/models/%s", organizationID, modelID), nil)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// ChatModelProviderDescriptor is the redacted view of an AI provider carried
// on the org model collection response. It carries only the capability
// metadata the Models UI needs; key material, base URLs, and headers are
// never exposed. The fields mirror the provider descriptors returned by the
// organization-scoped chat models collection.
type ChatModelProviderDescriptor struct {
ID uuid.UUID `json:"id" format:"uuid"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
Icon string `json:"icon"`
Enabled bool `json:"enabled"`
HasAPIKey bool `json:"has_api_key"`
HasUserAPIKey bool `json:"has_user_api_key"`
HasEffectiveAPIKey bool `json:"has_effective_api_key"`
AllowUserAPIKey bool `json:"allow_user_api_key"`
Available bool `json:"available"`
UnavailableReason ChatModelProviderUnavailableReason `json:"unavailable_reason,omitempty"`
}
// OrganizationChatModelsResponse is the org chat model config collection:
// the caller-readable configs plus the redacted provider descriptors the
// authoring page needs.
type OrganizationChatModelsResponse struct {
Models []ChatModel `json:"models"`
Providers []ChatModelProviderDescriptor `json:"providers"`
UnsupportedProviders []ChatUnsupportedProvider `json:"unsupported_providers"`
}
// GetChatCost returns the AI Gateway cost for the whole chat tree that
// contains chatID.
func (c *Client) GetChatCost(ctx context.Context, chatID uuid.UUID) (ChatCost, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/cost", chatID), nil)
if err != nil {
return ChatCost{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatCost{}, ReadBodyAsError(res)
}
var cost ChatCost
return cost, ReadBodyAsJSON(res, &cost)
}
// GetChatSystemPrompt returns the deployment-wide chat system prompt.
func (c *Client) GetChatSystemPrompt(ctx context.Context) (ChatSystemPromptResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/system-prompt", nil)
if err != nil {
return ChatSystemPromptResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatSystemPromptResponse{}, ReadBodyAsError(res)
}
var resp ChatSystemPromptResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateChatSystemPrompt updates the deployment-wide chat system prompt.
func (c *Client) UpdateChatSystemPrompt(ctx context.Context, req UpdateChatSystemPromptRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/system-prompt", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetChatPlanModeInstructions returns the deployment-wide plan mode instructions.
func (c *Client) GetChatPlanModeInstructions(ctx context.Context) (ChatPlanModeInstructionsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/plan-mode-instructions", nil)
if err != nil {
return ChatPlanModeInstructionsResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatPlanModeInstructionsResponse{}, ReadBodyAsError(res)
}
var resp ChatPlanModeInstructionsResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateChatPlanModeInstructions updates the deployment-wide plan mode instructions.
func (c *Client) UpdateChatPlanModeInstructions(ctx context.Context, req UpdateChatPlanModeInstructionsRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/plan-mode-instructions", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// OrganizationChatModelOverrides returns the configured chat model overrides for an organization.
func (c *Client) OrganizationChatModelOverrides(ctx context.Context, organizationID uuid.UUID) (ChatModelOverridesResponse, error) {
path := fmt.Sprintf("/api/v2/organizations/%s/chats/model-overrides", organizationID)
res, err := c.Request(ctx, http.MethodGet, path, nil)
if err != nil {
return ChatModelOverridesResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatModelOverridesResponse{}, ReadBodyAsError(res)
}
var resp ChatModelOverridesResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateOrganizationChatModelOverride updates or clears a chat model override for an organization.
func (c *Client) UpdateOrganizationChatModelOverride(ctx context.Context, organizationID uuid.UUID, override ChatModelOverrideContext, req UpdateChatModelOverrideRequest) (ChatModelOverrideResponse, error) {
path := fmt.Sprintf(
"/api/v2/organizations/%s/chats/model-overrides/%s",
organizationID,
url.PathEscape(string(override)),
)
res, err := c.Request(ctx, http.MethodPut, path, req)
if err != nil {
return ChatModelOverrideResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatModelOverrideResponse{}, ReadBodyAsError(res)
}
var resp ChatModelOverrideResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// OrganizationChatSystemPrompt returns the organization's chat system
// prompt. New chats in the organization receive it after the deployment
// system prompt.
func (c *Client) OrganizationChatSystemPrompt(ctx context.Context, organizationID uuid.UUID) (OrganizationChatSystemPromptResponse, error) {
path := fmt.Sprintf("/api/v2/organizations/%s/chats/config/system-prompt", organizationID)
res, err := c.Request(ctx, http.MethodGet, path, nil)
if err != nil {
return OrganizationChatSystemPromptResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return OrganizationChatSystemPromptResponse{}, ReadBodyAsError(res)
}
var resp OrganizationChatSystemPromptResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateOrganizationChatSystemPrompt replaces the organization's chat system
// prompt. An empty prompt clears it.
func (c *Client) UpdateOrganizationChatSystemPrompt(ctx context.Context, organizationID uuid.UUID, req UpdateOrganizationChatSystemPromptRequest) error {
path := fmt.Sprintf("/api/v2/organizations/%s/chats/config/system-prompt", organizationID)
res, err := c.Request(ctx, http.MethodPut, path, req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetChatPersonalModelOverridesAdminSettings returns the deployment-wide
// personal model override admin settings.
func (c *Client) GetChatPersonalModelOverridesAdminSettings(ctx context.Context) (ChatPersonalModelOverridesAdminSettings, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/personal-model-overrides", nil)
if err != nil {
return ChatPersonalModelOverridesAdminSettings{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatPersonalModelOverridesAdminSettings{}, ReadBodyAsError(res)
}
var resp ChatPersonalModelOverridesAdminSettings
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateChatPersonalModelOverridesAdminSettings updates the deployment-wide
// personal model override admin settings.
func (c *Client) UpdateChatPersonalModelOverridesAdminSettings(ctx context.Context, req UpdateChatPersonalModelOverridesAdminSettingsRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/personal-model-overrides", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// UserChatPersonalModelOverrides returns a user's personal model overrides in an organization.
func (c *Client) UserChatPersonalModelOverrides(ctx context.Context, organizationID uuid.UUID, user string) (UserChatPersonalModelOverridesResponse, error) {
path := fmt.Sprintf(
"/api/v2/organizations/%s/members/%s/chats/model-overrides",
organizationID,
url.PathEscape(user),
)
res, err := c.Request(ctx, http.MethodGet, path, nil)
if err != nil {
return UserChatPersonalModelOverridesResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserChatPersonalModelOverridesResponse{}, ReadBodyAsError(res)
}
var resp UserChatPersonalModelOverridesResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateUserChatPersonalModelOverride updates a user's personal model override in an organization.
func (c *Client) UpdateUserChatPersonalModelOverride(ctx context.Context, organizationID uuid.UUID, user string, override ChatPersonalModelOverrideContext, req UpdateUserChatPersonalModelOverrideRequest) error {
path := fmt.Sprintf(
"/api/v2/organizations/%s/members/%s/chats/model-overrides/%s",
organizationID,
url.PathEscape(user),
url.PathEscape(string(override)),
)
res, err := c.Request(ctx, http.MethodPut, path, req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetUserChatCustomPrompt fetches the user's custom chat prompt.
func (c *Client) GetUserChatCustomPrompt(ctx context.Context) (UserChatCustomPrompt, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/user-prompt", nil)
if err != nil {
return UserChatCustomPrompt{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserChatCustomPrompt{}, ReadBodyAsError(res)
}
var resp UserChatCustomPrompt
return resp, ReadBodyAsJSON(res, &resp)
}
// GetChatAdvisorConfig returns the deployment-wide advisor configuration.
func (c *ExperimentalClient) GetChatAdvisorConfig(ctx context.Context) (AdvisorConfig, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/advisor", nil)
if err != nil {
return AdvisorConfig{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return AdvisorConfig{}, ReadBodyAsError(res)
}
var resp AdvisorConfig
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateChatAdvisorConfig updates the deployment-wide advisor configuration.
func (c *ExperimentalClient) UpdateChatAdvisorConfig(ctx context.Context, req UpdateAdvisorConfigRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/advisor", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetChatComputerUseProvider returns the deployment-wide computer use provider.
func (c *ExperimentalClient) GetChatComputerUseProvider(ctx context.Context) (ChatComputerUseProviderResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/computer-use-provider", nil)
if err != nil {
return ChatComputerUseProviderResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatComputerUseProviderResponse{}, ReadBodyAsError(res)
}
var resp ChatComputerUseProviderResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateChatComputerUseProvider updates the deployment-wide computer use
// provider.
func (c *ExperimentalClient) UpdateChatComputerUseProvider(ctx context.Context, req UpdateChatComputerUseProviderRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/computer-use-provider", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetChatWorkspaceTTL returns the configured chat workspace TTL.
func (c *Client) GetChatWorkspaceTTL(ctx context.Context) (ChatWorkspaceTTLResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/workspace-ttl", nil)
if err != nil {
return ChatWorkspaceTTLResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatWorkspaceTTLResponse{}, ReadBodyAsError(res)
}
var resp ChatWorkspaceTTLResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateChatWorkspaceTTL updates the chat workspace TTL setting.
func (c *Client) UpdateChatWorkspaceTTL(ctx context.Context, req UpdateChatWorkspaceTTLRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/workspace-ttl", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetChatRetentionDays returns the configured chat retention period.
func (c *Client) GetChatRetentionDays(ctx context.Context) (ChatRetentionDaysResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/retention-days", nil)
if err != nil {
return ChatRetentionDaysResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatRetentionDaysResponse{}, ReadBodyAsError(res)
}
var resp ChatRetentionDaysResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateChatRetentionDays updates the chat retention period.
func (c *Client) UpdateChatRetentionDays(ctx context.Context, req UpdateChatRetentionDaysRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/retention-days", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetChatDebugRetentionDays returns the configured chat debug run
// retention period.
func (c *Client) GetChatDebugRetentionDays(ctx context.Context) (ChatDebugRetentionDaysResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/debug-retention-days", nil)
if err != nil {
return ChatDebugRetentionDaysResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatDebugRetentionDaysResponse{}, ReadBodyAsError(res)
}
var resp ChatDebugRetentionDaysResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateChatDebugRetentionDays updates the chat debug run retention period.
func (c *Client) UpdateChatDebugRetentionDays(ctx context.Context, req UpdateChatDebugRetentionDaysRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/debug-retention-days", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetChatAutoArchiveDays returns the configured chat auto-archive period.
func (c *Client) GetChatAutoArchiveDays(ctx context.Context) (ChatAutoArchiveDaysResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/auto-archive-days", nil)
if err != nil {
return ChatAutoArchiveDaysResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatAutoArchiveDaysResponse{}, ReadBodyAsError(res)
}
var resp ChatAutoArchiveDaysResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateChatAutoArchiveDays updates the chat auto-archive period.
func (c *Client) UpdateChatAutoArchiveDays(ctx context.Context, req UpdateChatAutoArchiveDaysRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/auto-archive-days", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// UpdateUserChatCustomPrompt updates the user's custom chat prompt.
func (c *Client) UpdateUserChatCustomPrompt(ctx context.Context, req UserChatCustomPrompt) (UserChatCustomPrompt, error) {
res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/user-prompt", req)
if err != nil {
return UserChatCustomPrompt{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserChatCustomPrompt{}, ReadBodyAsError(res)
}
var resp UserChatCustomPrompt
return resp, ReadBodyAsJSON(res, &resp)
}
// GetUserChatCompactionThresholds fetches the user's per-model chat
// compaction thresholds.
func (c *Client) GetUserChatCompactionThresholds(ctx context.Context) (UserChatCompactionThresholds, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/user-compaction-thresholds", nil)
if err != nil {
return UserChatCompactionThresholds{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserChatCompactionThresholds{}, ReadBodyAsError(res)
}
var thresholds UserChatCompactionThresholds
return thresholds, ReadBodyAsJSON(res, &thresholds)
}
// UpdateUserChatCompactionThreshold updates the user's per-model chat
// compaction threshold.
func (c *Client) UpdateUserChatCompactionThreshold(ctx context.Context, modelID uuid.UUID, req UpdateUserChatCompactionThresholdRequest) (UserChatCompactionThreshold, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/chats/config/user-compaction-thresholds/%s", modelID), req)
if err != nil {
return UserChatCompactionThreshold{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserChatCompactionThreshold{}, ReadBodyAsError(res)
}
var threshold UserChatCompactionThreshold
return threshold, ReadBodyAsJSON(res, &threshold)
}
// DeleteUserChatCompactionThreshold deletes the user's per-model chat
// compaction threshold override.
func (c *Client) DeleteUserChatCompactionThreshold(ctx context.Context, modelID uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/chats/config/user-compaction-thresholds/%s", modelID), nil)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// CreateChat creates a new chat.
func (c *Client) CreateChat(ctx context.Context, req CreateChatRequest) (Chat, error) {
res, err := c.Request(ctx, http.MethodPost, "/api/v2/chats", req)
if err != nil {
return Chat{}, err
}
if res.StatusCode != http.StatusCreated {
return Chat{}, ReadBodyAsError(res)
}
defer res.Body.Close()
var chat Chat
return chat, ReadBodyAsJSON(res, &chat)
}
// StreamChatOptions are optional parameters for StreamChat.
type StreamChatOptions struct {
// AfterID limits the initial snapshot to messages created
// after the given ID. This is useful for relay connections
// that only need live message_part events and can skip the
// full message history.
AfterID *int64
}
// StreamChat streams chat updates in real time.
//
// The returned channel includes initial snapshot events first, followed by
// live updates. Callers must close the returned io.Closer to release the
// websocket connection when done.
func (c *Client) StreamChat(ctx context.Context, chatID uuid.UUID, opts *StreamChatOptions) (<-chan ChatStreamEvent, io.Closer, error) {
path := fmt.Sprintf("/api/v2/chats/%s/stream", chatID)
if opts != nil && opts.AfterID != nil {
path += fmt.Sprintf("?after_id=%d", *opts.AfterID)
}
conn, err := c.Dial(
ctx,
path,
&websocket.DialOptions{CompressionMode: websocket.CompressionDisabled},
)
if err != nil {
return nil, nil, err
}
conn.SetReadLimit(1 << 22) // 4MiB
streamCtx, streamCancel := context.WithCancel(ctx)
events := make(chan ChatStreamEvent, 128)
send := func(event ChatStreamEvent) bool {
if event.ChatID == uuid.Nil {
event.ChatID = chatID
}
select {
case <-streamCtx.Done():
return false
case events <- event:
return true
}
}
go func() {
defer close(events)
defer streamCancel()
defer func() {
_ = conn.Close(websocket.StatusNormalClosure, "")
}()
for {
var batch []ChatStreamEvent
if err := wsjson.Read(streamCtx, conn, &batch); err != nil {
if streamCtx.Err() != nil {
return
}
switch websocket.CloseStatus(err) {
case websocket.StatusNormalClosure, websocket.StatusGoingAway:
return
}
_ = send(ChatStreamEvent{
Type: ChatStreamEventTypeError,
Error: &ChatError{
Message: fmt.Sprintf("read chat stream: %v", err),
},
})
return
}
for _, event := range batch {
if !send(event) {
return
}
}
}
}()
return events, closeFunc(func() error {
streamCancel()
return nil
}), nil
}
// WatchChats streams lifecycle events for all of the authenticated
// user's chats in real time. The returned channel emits
// ChatWatchEvent values for status changes, title changes, creation,
// deletion, diff-status changes, and action-required notifications.
// Callers must close the returned io.Closer to release the websocket
// connection when done.
func (c *Client) WatchChats(ctx context.Context) (<-chan ChatWatchEvent, io.Closer, error) {
conn, err := c.Dial(
ctx,
"/api/v2/chats/watch",
&websocket.DialOptions{CompressionMode: websocket.CompressionDisabled},
)
if err != nil {
return nil, nil, err
}
conn.SetReadLimit(1 << 22) // 4MiB
streamCtx, streamCancel := context.WithCancel(ctx)
events := make(chan ChatWatchEvent, 128)
go func() {
defer close(events)
defer streamCancel()
defer func() {
_ = conn.Close(websocket.StatusNormalClosure, "")
}()
for {
var event ChatWatchEvent
if err := wsjson.Read(streamCtx, conn, &event); err != nil {
if streamCtx.Err() != nil {
return
}
switch websocket.CloseStatus(err) {
case websocket.StatusNormalClosure, websocket.StatusGoingAway:
return
}
return
}
select {
case <-streamCtx.Done():
return
case events <- event:
}
}
}()
return events, closeFunc(func() error {
streamCancel()
return nil
}), nil
}
// GetChatDebugLogging returns the runtime admin setting that allows
// users to opt into chat debug logging.
func (c *Client) GetChatDebugLogging(ctx context.Context) (ChatDebugLoggingAdminSettings, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/debug-logging", nil)
if err != nil {
return ChatDebugLoggingAdminSettings{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatDebugLoggingAdminSettings{}, ReadBodyAsError(res)
}
var resp ChatDebugLoggingAdminSettings
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateChatDebugLogging updates the runtime admin setting that allows
// users to opt into chat debug logging.
func (c *Client) UpdateChatDebugLogging(ctx context.Context, req UpdateChatDebugLoggingAllowUsersRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/debug-logging", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetUserChatDebugLogging returns whether chat debug logging is active
// for the current user and whether the user may change it.
func (c *Client) GetUserChatDebugLogging(ctx context.Context) (UserChatDebugLoggingSettings, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats/config/user-debug-logging", nil)
if err != nil {
return UserChatDebugLoggingSettings{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserChatDebugLoggingSettings{}, ReadBodyAsError(res)
}
var resp UserChatDebugLoggingSettings
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateUserChatDebugLogging updates the current user's chat debug
// logging preference.
func (c *Client) UpdateUserChatDebugLogging(ctx context.Context, req UpdateUserChatDebugLoggingRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/v2/chats/config/user-debug-logging", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetChatDebugRuns returns the debug runs for a chat.
func (c *ExperimentalClient) GetChatDebugRuns(ctx context.Context, chatID uuid.UUID) ([]ChatDebugRunSummary, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/debug/runs", chatID), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var resp []ChatDebugRunSummary
return resp, ReadBodyAsJSON(res, &resp)
}
// GetChatDebugRun returns a single debug run along with its full step
// history. Use GetChatDebugRuns when only the run summary list is needed.
func (c *ExperimentalClient) GetChatDebugRun(ctx context.Context, chatID uuid.UUID, runID uuid.UUID) (ChatDebugRun, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/debug/runs/%s", chatID, runID), nil)
if err != nil {
return ChatDebugRun{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatDebugRun{}, ReadBodyAsError(res)
}
var resp ChatDebugRun
return resp, ReadBodyAsJSON(res, &resp)
}
// GetChat returns a chat by ID.
func (c *Client) GetChat(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s", chatID), nil)
if err != nil {
return Chat{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Chat{}, ReadBodyAsError(res)
}
var chat Chat
return chat, ReadBodyAsJSON(res, &chat)
}
// RefreshChatContext re-pins the chat to its agent's latest context snapshot
// and clears the dirty marker. The request takes no body.
func (c *Client) RefreshChatContext(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/chats/%s/context", chatID), nil)
if err != nil {
return Chat{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Chat{}, ReadBodyAsError(res)
}
var chat Chat
return chat, ReadBodyAsJSON(res, &chat)
}
func (c *Client) GetChatACL(ctx context.Context, chatID uuid.UUID) (ChatACL, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/acl", chatID), nil)
if err != nil {
return ChatACL{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatACL{}, ReadBodyAsError(res)
}
var acl ChatACL
return acl, ReadBodyAsJSON(res, &acl)
}
func (c *Client) UpdateChatACL(ctx context.Context, chatID uuid.UUID, req UpdateChatACL) error {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/chats/%s/acl", chatID), req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetChatMessages returns the messages and queued messages for a chat.
// ChatMessagesPaginationOptions are optional pagination params for
// GetChatMessages.
type ChatMessagesPaginationOptions struct {
BeforeID int64
// AfterID, when > 0, restricts results to messages with id strictly
// greater than AfterID. When set without BeforeID, results come back
// in ASCENDING id order so a polling caller can advance its cursor
// to max(returned_ids) without gaps. When combined with BeforeID,
// results come back in DESC order over the open range
// (AfterID, BeforeID).
AfterID int64
Limit int
}
// GetChatMessages returns the messages and queued messages for a chat.
func (c *Client) GetChatMessages(ctx context.Context, chatID uuid.UUID, opts *ChatMessagesPaginationOptions) (ChatMessagesResponse, error) {
reqOpts := []RequestOption{}
if opts != nil {
reqOpts = append(reqOpts, func(r *http.Request) {
q := r.URL.Query()
if opts.BeforeID > 0 {
q.Set("before_id", strconv.FormatInt(opts.BeforeID, 10))
}
if opts.AfterID > 0 {
q.Set("after_id", strconv.FormatInt(opts.AfterID, 10))
}
if opts.Limit > 0 {
q.Set("limit", strconv.Itoa(opts.Limit))
}
r.URL.RawQuery = q.Encode()
})
}
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/messages", chatID), nil, reqOpts...)
if err != nil {
return ChatMessagesResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatMessagesResponse{}, ReadBodyAsError(res)
}
var resp ChatMessagesResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// ChatPromptsOptions are optional query parameters for GetChatPrompts.
type ChatPromptsOptions struct {
// Limit caps the number of prompts returned. The server enforces a
// minimum of 1 and a maximum of 2000; passing 0 (or negative)
// applies the server-side default of 500.
Limit int
}
// GetChatPrompts returns the user prompts for a chat in newest-first
// order. It is a thin endpoint dedicated to the composer's prompt
// history cycle: only user-visible user messages are included, and
// only their text parts (concatenated in the original order) are
// returned. Whitespace-only prompts are filtered server-side so the
// caller never has to skip blank entries while cycling.
func (c *Client) GetChatPrompts(ctx context.Context, chatID uuid.UUID, opts *ChatPromptsOptions) (ChatPromptsResponse, error) {
reqOpts := []RequestOption{}
if opts != nil && opts.Limit > 0 {
reqOpts = append(reqOpts, func(r *http.Request) {
q := r.URL.Query()
q.Set("limit", strconv.Itoa(opts.Limit))
r.URL.RawQuery = q.Encode()
})
}
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/prompts", chatID), nil, reqOpts...)
if err != nil {
return ChatPromptsResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatPromptsResponse{}, ReadBodyAsError(res)
}
var resp ChatPromptsResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UpdateChat patches a chat resource.
func (c *Client) UpdateChat(ctx context.Context, chatID uuid.UUID, req UpdateChatRequest) error {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/chats/%s", chatID), req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// CreateChatMessage adds a message to a chat.
func (c *Client) CreateChatMessage(ctx context.Context, chatID uuid.UUID, req CreateChatMessageRequest) (CreateChatMessageResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/messages", chatID), req)
if err != nil {
return CreateChatMessageResponse{}, err
}
if res.StatusCode != http.StatusOK {
return CreateChatMessageResponse{}, ReadBodyAsError(res)
}
defer res.Body.Close()
var resp CreateChatMessageResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// EditChatMessage edits an existing user message in a chat and re-runs from there.
func (c *Client) EditChatMessage(
ctx context.Context,
chatID uuid.UUID,
messageID int64,
req EditChatMessageRequest,
) (EditChatMessageResponse, error) {
res, err := c.Request(
ctx,
http.MethodPatch,
fmt.Sprintf("/api/v2/chats/%s/messages/%d", chatID, messageID),
req,
)
if err != nil {
return EditChatMessageResponse{}, err
}
if res.StatusCode != http.StatusOK {
return EditChatMessageResponse{}, ReadBodyAsError(res)
}
defer res.Body.Close()
var resp EditChatMessageResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// InterruptChat cancels an in-flight chat run and leaves it waiting.
func (c *Client) InterruptChat(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/interrupt", chatID), nil)
if err != nil {
return Chat{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Chat{}, ReadBodyAsError(res)
}
var chat Chat
return chat, ReadBodyAsJSON(res, &chat)
}
// CompactChat requests a manual context compaction on an idle or
// errored chat, clearing any stored error. The compaction runs
// asynchronously through the chat worker and bypasses the automatic
// usage threshold.
func (c *Client) CompactChat(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/compact", chatID), nil)
if err != nil {
return Chat{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Chat{}, ReadBodyAsError(res)
}
var chat Chat
return chat, ReadBodyAsJSON(res, &chat)
}
// ClearChat resets the model context of an idle or errored chat,
// clearing any stored error. The reset commits synchronously with no
// model call: the transcript is preserved and the next prompt starts
// from a fresh context.
func (c *Client) ClearChat(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/clear", chatID), nil)
if err != nil {
return Chat{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Chat{}, ReadBodyAsError(res)
}
var chat Chat
return chat, ReadBodyAsJSON(res, &chat)
}
// ReconcileInvalidChatState recovers a chat stuck in an invalid
// execution state, moving it into an error state from which the caller
// can send a new message or edit history to continue.
func (c *Client) ReconcileInvalidChatState(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/reconcile-invalid", chatID), nil)
if err != nil {
return Chat{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Chat{}, ReadBodyAsError(res)
}
var chat Chat
return chat, ReadBodyAsJSON(res, &chat)
}
// ProposeChatTitleResponse is returned by the propose-title endpoint.
type ProposeChatTitleResponse struct {
Title string `json:"title"`
}
// ProposeChatTitle requests the server to generate a suggested chat title without persisting it.
func (c *Client) ProposeChatTitle(ctx context.Context, chatID uuid.UUID) (ProposeChatTitleResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/title/propose", chatID), nil)
if err != nil {
return ProposeChatTitleResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ProposeChatTitleResponse{}, ReadBodyAsError(res)
}
var resp ProposeChatTitleResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// GetChatDiffContents returns resolved diff contents for a chat.
// Without options, it returns the primary ref's diff. Pass
// WithChatDiffStatusRef to select another ref the chat tracks.
func (c *Client) GetChatDiffContents(
ctx context.Context,
chatID uuid.UUID,
opts ...RequestOption,
) (ChatDiffContents, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/diff", chatID), nil, opts...)
if err != nil {
return ChatDiffContents{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatDiffContents{}, ReadBodyAsError(res)
}
var diff ChatDiffContents
return diff, ReadBodyAsJSON(res, &diff)
}
// WithChatDiffStatusRef selects the ref for GetChatDiffContents.
// The ref must match a ref that the chat tracks.
func WithChatDiffStatusRef(ref DiffStatusRef) RequestOption {
return func(r *http.Request) {
WithQueryParam("origin", ref.RemoteOrigin)(r)
WithQueryParam("branch", ref.GitBranch)(r)
}
}
// UploadChatFile uploads a file for use in chat messages.
func (c *Client) UploadChatFile(ctx context.Context, organizationID uuid.UUID, contentType string, filename string, rd io.Reader) (UploadChatFileResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/files?organization=%s", organizationID), rd, func(r *http.Request) {
r.Header.Set("Content-Type", contentType)
if filename != "" {
r.Header.Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
}
})
if err != nil {
return UploadChatFileResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return UploadChatFileResponse{}, ReadBodyAsError(res)
}
var resp UploadChatFileResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// ChatFileDownloadURL creates a short-lived download URL for a chat file.
func (c *Client) ChatFileDownloadURL(ctx context.Context, fileID uuid.UUID) (ChatFileDownloadURLResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/files/%s/download-url", fileID), nil)
if err != nil {
return ChatFileDownloadURLResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatFileDownloadURLResponse{}, ReadBodyAsError(res)
}
var resp ChatFileDownloadURLResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// UploadChatWorkspaceFile streams a file to the chat's workspace
// filesystem via the workspace agent. There is no server-imposed size
// cap; canceling ctx aborts the stream and no partial target file is
// left behind (the agent removes the target when the write fails).
func (c *Client) UploadChatWorkspaceFile(ctx context.Context, chatID uuid.UUID, contentType, filename string, rd io.Reader) (UploadChatWorkspaceFileResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/workspace-files", chatID), rd, func(r *http.Request) {
if contentType != "" {
r.Header.Set("Content-Type", contentType)
} else {
// Drop the SDK's default application/json so the server
// can sniff-default an undeclared type.
r.Header.Del("Content-Type")
}
if filename != "" {
r.Header.Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
}
})
if err != nil {
return UploadChatWorkspaceFileResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return UploadChatWorkspaceFileResponse{}, ReadBodyAsError(res)
}
var resp UploadChatWorkspaceFileResponse
return resp, ReadBodyAsJSON(res, &resp)
}
// GetChatFile retrieves a previously uploaded chat file by ID.
func (c *Client) GetChatFile(ctx context.Context, fileID uuid.UUID) ([]byte, string, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/files/%s", fileID), nil)
if err != nil {
return nil, "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, "", ReadBodyAsError(res)
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, "", err
}
return data, res.Header.Get("Content-Type"), nil
}
// SubmitToolResults submits the results of dynamic tool calls for a chat
// that is in requires_action status.
func (c *Client) SubmitToolResults(ctx context.Context, chatID uuid.UUID, req SubmitToolResultsRequest) error {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/tool-results", chatID), req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetChatsByWorkspace returns a mapping of workspace ID to the latest
// non-archived chat ID for each requested workspace. Workspaces with
// no chats are omitted from the response.
func (c *Client) GetChatsByWorkspace(ctx context.Context, workspaceIDs []uuid.UUID) (map[uuid.UUID]uuid.UUID, error) {
ids := make([]string, 0, len(workspaceIDs))
for _, id := range workspaceIDs {
ids = append(ids, id.String())
}
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/by-workspace?workspace_ids=%s", strings.Join(ids, ",")), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var result map[uuid.UUID]uuid.UUID
return result, ReadBodyAsJSON(res, &result)
}