Files
coder/codersdk/chats.go
T
Ethan d2f9280138 chore: remove legacy chat template allowlist (#27515)
Relates to CODAGT-713

Depends on #27514

Removes the legacy deployment-wide allowlist now that the API and frontend use per-template `agents_allowed`: the experimental `/template-allowlist` routes, SDK methods and generated types, site config queries, frontend bindings, and the now-unused `xjson` utility.

Migration `000563` deletes the obsolete `agents_template_allowlist` value. It's irreversible for deployments that configured an allowlist, which I think is fine, since `000562` already drops `agents_allowed` on the way down, and this release ships `000548` and `000555` with the same property.

Two side effects of the model change worth writing down, both from #27514 rather than here. The value used to need `ActionRead` on `ResourceDeploymentConfig` to read and deployment config update to write. `AgentsAllowed` is now a plain field on the template response, readable by anyone who can read the template, and it's set with a template update, so org admins manage it themselves. That's the delegation we wanted, and it's tracked in the audit log.

The rest of the stack adds `--agents-allowed` to the CLI and updates the platform controls docs.
2026-08-06 14:35:37 +10:00

3217 lines
136 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:"
// MaxChatFileIDs is the maximum number of file IDs that can be
// associated with a single chat. This limit prevents unbounded
// growth in the chat_file_links table. It is easier to raise
// this limit than to lower it.
const MaxChatFileIDs = 50
// MaxChatFileSizeBytes is the upload-endpoint cap for chat
// attachments.
const MaxChatFileSizeBytes = 10 * 1024 * 1024
// 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"
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,
ChatAttachmentMediaTypeImageWEBP,
ChatAttachmentMediaTypeTextCSV,
ChatAttachmentMediaTypeTextMarkdown,
ChatAttachmentMediaTypeTextPlain,
}
// CompactionThresholdKey returns the user-config key for a specific
// model configuration's compaction threshold.
func CompactionThresholdKey(modelConfigID uuid.UUID) string {
return ChatCompactionThresholdKeyPrefix + modelConfigID.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"
)
// 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"`
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"`
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 *ChatDiffStatus `json:"diff_status,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.
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"`
Warnings []string `json:"warnings,omitempty"`
ClientType ChatClientType `json:"client_type"`
// 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"`
}
// 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"`
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"`
}
// 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 *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"
// 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,
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?"`
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:"-"`
// 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?"`
// 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:"-"`
}
// StripInternal removes internal-only fields that must not be
// sent to API clients. Call before publishing via REST or SSE.
//
// Note: ArgsDelta, ResultDelta, and ResultReset are intentionally preserved.
// They are streaming-only fields consumed by the frontend via SSE
// message_part events. ArgsDelta is produced by processStepStream in
// chatloop; ResultDelta and ResultReset are produced by the advisor
// streaming callbacks in chatd.
func (p *ChatMessagePart) StripInternal() {
p.ProviderMetadata = nil
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,
}
}
// 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"
)
// 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"`
}
// 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"`
}
// CreateChatRequest is the request to create a new chat.
type CreateChatRequest struct {
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
Content []ChatInputPart `json:"content"`
SystemPrompt string `json:"system_prompt,omitempty"`
WorkspaceID *uuid.UUID `json:"workspace_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"`
PlanMode ChatPlanMode `json:"plan_mode,omitempty"`
ClientType ChatClientType `json:"client_type,omitempty"`
}
// UpdateChatRequest is the request to update a chat.
type UpdateChatRequest struct {
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"`
// 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"`
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"`
}
// 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"`
}
// 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"`
}
// ChatPrompt is a single user-authored prompt in a chat, returned by
// GET /api/experimental/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/experimental/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"
)
// ChatModel represents a model in the chat model catalog.
type ChatModel struct {
ID string `json:"id"`
Provider string `json:"provider"`
Model string `json:"model"`
DisplayName string `json:"display_name"`
}
// ChatModelProvider represents provider availability and model results.
type ChatModelProvider struct {
Provider string `json:"provider"`
Available bool `json:"available"`
UnavailableReason ChatModelProviderUnavailableReason `json:"unavailable_reason,omitempty"`
Models []ChatModel `json:"models"`
}
// ChatModelsResponse is the catalog returned from chat model discovery.
type ChatModelsResponse struct {
Providers []ChatModelProvider `json:"providers"`
// UnsupportedProviders lists configured providers the Agents harness
// cannot use, so the UI can explain the empty state.
UnsupportedProviders []ChatUnsupportedProvider `json:"unsupported_providers"`
}
// 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"`
}
// 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 a
// deployment override applies to.
type ChatModelOverrideContext string
const (
ChatModelOverrideContextGeneral ChatModelOverrideContext = "general"
ChatModelOverrideContextExplore ChatModelOverrideContext = "explore"
ChatModelOverrideContextTitleGeneration ChatModelOverrideContext = "title_generation"
ChatModelOverrideContextCompaction ChatModelOverrideContext = "compaction"
)
// Valid reports whether the override context is one of the supported values.
func (c ChatModelOverrideContext) Valid() bool {
switch c {
case ChatModelOverrideContextGeneral,
ChatModelOverrideContextExplore,
ChatModelOverrideContextTitleGeneration,
ChatModelOverrideContextCompaction:
return true
default:
return false
}
}
// AllChatModelOverrideContexts returns all supported override contexts.
func AllChatModelOverrideContexts() []ChatModelOverrideContext {
return []ChatModelOverrideContext{
ChatModelOverrideContextGeneral,
ChatModelOverrideContextExplore,
ChatModelOverrideContextTitleGeneration,
ChatModelOverrideContextCompaction,
}
}
// ChatModelOverrideResponse is the response body for the chat model override
// configuration endpoint.
type ChatModelOverrideResponse struct {
Context ChatModelOverrideContext `json:"context"`
ModelConfigID string `json:"model_config_id"`
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
IsMalformed bool `json:"is_malformed"`
}
// 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"`
IsMalformed bool `json:"is_malformed"`
}
// 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"`
// ModelConfigID selects a specific chat model config to power the
// advisor. uuid.Nil means reuse the outer chat model. The runtime
// must fall back to the outer chat model when this ID cannot be
// resolved (e.g. the referenced model config was soft-deleted or
// its provider was disabled after the admin saved this config).
ModelConfigID uuid.UUID `json:"model_config_id" format:"uuid"`
// ReasoningEffort overrides the selected advisor model's configured default.
// It requires a non-zero ModelConfigID.
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
}
// UpdateAdvisorConfigRequest is the request body for updating advisor
// runtime configuration. It is a type alias for AdvisorConfig because
// the request and response shapes are currently identical.
type UpdateAdvisorConfigRequest = AdvisorConfig
// 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"`
}
// CreateChatProviderConfigRequest creates a chat provider config.
type CreateChatProviderConfigRequest struct {
Provider string `json:"provider"`
DisplayName string `json:"display_name,omitempty"`
Icon string `json:"icon,omitempty"`
APIKey string `json:"api_key,omitempty"`
BaseURL string `json:"base_url,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
CentralAPIKeyEnabled *bool `json:"central_api_key_enabled,omitempty"`
AllowUserAPIKey *bool `json:"allow_user_api_key,omitempty"`
AllowCentralAPIKeyFallback *bool `json:"allow_central_api_key_fallback,omitempty"`
}
// UpdateChatProviderConfigRequest updates a chat provider config.
type UpdateChatProviderConfigRequest struct {
DisplayName string `json:"display_name,omitempty"`
Icon string `json:"icon,omitempty"`
APIKey *string `json:"api_key,omitempty"`
BaseURL *string `json:"base_url,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
CentralAPIKeyEnabled *bool `json:"central_api_key_enabled,omitempty"`
AllowUserAPIKey *bool `json:"allow_user_api_key,omitempty"`
AllowCentralAPIKeyFallback *bool `json:"allow_central_api_key_fallback,omitempty"`
}
// 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"`
}
// ChatModelConfig is an admin-managed model configuration.
type ChatModelConfig struct {
ID uuid.UUID `json:"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"`
}
// 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"`
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"`
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.
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."`
}
// 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
}
// CreateChatModelConfigRequest creates a chat model config.
type CreateChatModelConfigRequest 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"`
}
// UpdateChatModelConfigRequest updates a chat model config.
type UpdateChatModelConfigRequest 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"`
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"`
}
// 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/experimental/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/experimental/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/experimental/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 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"`
}
// 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
Pagination
}
// ListChats returns all chats for the authenticated user.
func (c *ExperimentalClient) ListChats(ctx context.Context, opts *ListChatsOptions) ([]Chat, error) {
var reqOpts []RequestOption
if opts != nil {
reqOpts = append(reqOpts, opts.Pagination.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 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/experimental/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)
}
// ListChatModels returns the available chat model catalog.
func (c *ExperimentalClient) ListChatModels(ctx context.Context) (ChatModelsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/models", nil)
if err != nil {
return ChatModelsResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatModelsResponse{}, ReadBodyAsError(res)
}
var catalog ChatModelsResponse
return catalog, ReadBodyAsJSON(res, &catalog)
}
// ListChatProviders returns admin-managed chat provider configs.
func (c *ExperimentalClient) ListChatProviders(ctx context.Context) ([]ChatProviderConfig, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/providers", nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var providers []ChatProviderConfig
return providers, ReadBodyAsJSON(res, &providers)
}
// CreateChatProvider creates an admin-managed chat provider config.
func (c *ExperimentalClient) CreateChatProvider(ctx context.Context, req CreateChatProviderConfigRequest) (ChatProviderConfig, error) {
res, err := c.Request(ctx, http.MethodPost, "/api/experimental/chats/providers", req)
if err != nil {
return ChatProviderConfig{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return ChatProviderConfig{}, ReadBodyAsError(res)
}
var provider ChatProviderConfig
return provider, ReadBodyAsJSON(res, &provider)
}
// UpdateChatProvider updates an admin-managed chat provider config.
func (c *ExperimentalClient) UpdateChatProvider(ctx context.Context, providerID uuid.UUID, req UpdateChatProviderConfigRequest) (ChatProviderConfig, error) {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/chats/providers/%s", providerID), req)
if err != nil {
return ChatProviderConfig{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatProviderConfig{}, ReadBodyAsError(res)
}
var provider ChatProviderConfig
return provider, ReadBodyAsJSON(res, &provider)
}
// DeleteChatProvider deletes an admin-managed chat provider config.
func (c *ExperimentalClient) DeleteChatProvider(ctx context.Context, providerID uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/providers/%s", providerID), 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 *ExperimentalClient) 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 *ExperimentalClient) 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 *ExperimentalClient) 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/experimental/users/%s/ai-provider-keys", url.PathEscape(user))
}
// ListUserChatProviderConfigs returns user-scoped chat provider configs.
func (c *ExperimentalClient) ListUserChatProviderConfigs(ctx context.Context) ([]UserChatProviderConfig, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/user-provider-configs", nil)
if err != nil {
return nil, xerrors.Errorf("list user chat provider configs: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var configs []UserChatProviderConfig
return configs, ReadBodyAsJSON(res, &configs)
}
// UpsertUserChatProviderKey creates or replaces a user API key for a provider.
func (c *ExperimentalClient) UpsertUserChatProviderKey(ctx context.Context, providerID uuid.UUID, req CreateUserChatProviderKeyRequest) (UserChatProviderConfig, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/experimental/chats/user-provider-configs/%s", providerID), req)
if err != nil {
return UserChatProviderConfig{}, xerrors.Errorf("upsert user chat provider key: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserChatProviderConfig{}, ReadBodyAsError(res)
}
var config UserChatProviderConfig
return config, ReadBodyAsJSON(res, &config)
}
// DeleteUserChatProviderKey deletes a user API key for a provider.
func (c *ExperimentalClient) DeleteUserChatProviderKey(ctx context.Context, providerID uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/user-provider-configs/%s", providerID), nil)
if err != nil {
return xerrors.Errorf("delete user chat provider key: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// ListChatModelConfigs returns admin-managed chat model configs.
func (c *ExperimentalClient) ListChatModelConfigs(ctx context.Context) ([]ChatModelConfig, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/model-configs", nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var configs []ChatModelConfig
return configs, ReadBodyAsJSON(res, &configs)
}
// CreateChatModelConfig creates an admin-managed chat model config.
func (c *ExperimentalClient) CreateChatModelConfig(ctx context.Context, req CreateChatModelConfigRequest) (ChatModelConfig, error) {
res, err := c.Request(ctx, http.MethodPost, "/api/experimental/chats/model-configs", req)
if err != nil {
return ChatModelConfig{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return ChatModelConfig{}, ReadBodyAsError(res)
}
var config ChatModelConfig
return config, ReadBodyAsJSON(res, &config)
}
// UpdateChatModelConfig updates an admin-managed chat model config.
func (c *ExperimentalClient) UpdateChatModelConfig(ctx context.Context, modelConfigID uuid.UUID, req UpdateChatModelConfigRequest) (ChatModelConfig, error) {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/chats/model-configs/%s", modelConfigID), req)
if err != nil {
return ChatModelConfig{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ChatModelConfig{}, ReadBodyAsError(res)
}
var config ChatModelConfig
return config, ReadBodyAsJSON(res, &config)
}
// DeleteChatModelConfig deletes an admin-managed chat model config.
func (c *ExperimentalClient) DeleteChatModelConfig(ctx context.Context, modelConfigID uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/model-configs/%s", modelConfigID), nil)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetChatCost returns the AI Gateway cost for the whole chat tree that
// contains chatID.
func (c *ExperimentalClient) GetChatCost(ctx context.Context, chatID uuid.UUID) (ChatCost, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/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 *ExperimentalClient) GetChatSystemPrompt(ctx context.Context) (ChatSystemPromptResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/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 *ExperimentalClient) UpdateChatSystemPrompt(ctx context.Context, req UpdateChatSystemPromptRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/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 *ExperimentalClient) GetChatPlanModeInstructions(ctx context.Context) (ChatPlanModeInstructionsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/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 *ExperimentalClient) UpdateChatPlanModeInstructions(ctx context.Context, req UpdateChatPlanModeInstructionsRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/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
}
// GetChatModelOverride returns the deployment-wide chat model override for
// the requested context.
func (c *ExperimentalClient) GetChatModelOverride(ctx context.Context, override ChatModelOverrideContext) (ChatModelOverrideResponse, error) {
path := fmt.Sprintf(
"/api/experimental/chats/config/model-override/%s",
url.PathEscape(string(override)),
)
res, err := c.Request(ctx, http.MethodGet, path, nil)
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)
}
// UpdateChatModelOverride updates the deployment-wide chat model override for
// the requested context.
func (c *ExperimentalClient) UpdateChatModelOverride(ctx context.Context, override ChatModelOverrideContext, req UpdateChatModelOverrideRequest) error {
path := fmt.Sprintf(
"/api/experimental/chats/config/model-override/%s",
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
}
// GetChatPersonalModelOverridesAdminSettings returns the deployment-wide
// personal model override admin settings.
func (c *ExperimentalClient) GetChatPersonalModelOverridesAdminSettings(ctx context.Context) (ChatPersonalModelOverridesAdminSettings, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/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 *ExperimentalClient) UpdateChatPersonalModelOverridesAdminSettings(ctx context.Context, req UpdateChatPersonalModelOverridesAdminSettingsRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/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
}
// GetUserChatPersonalModelOverrides fetches the user's personal model
// override settings.
func (c *ExperimentalClient) GetUserChatPersonalModelOverrides(ctx context.Context) (UserChatPersonalModelOverridesResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/user-personal-model-overrides", 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 the user's personal model
// override for the requested context.
func (c *ExperimentalClient) UpdateUserChatPersonalModelOverride(ctx context.Context, override ChatPersonalModelOverrideContext, req UpdateUserChatPersonalModelOverrideRequest) error {
path := fmt.Sprintf(
"/api/experimental/chats/config/user-personal-model-overrides/%s",
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 *ExperimentalClient) GetUserChatCustomPrompt(ctx context.Context) (UserChatCustomPrompt, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/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 *ExperimentalClient) GetChatWorkspaceTTL(ctx context.Context) (ChatWorkspaceTTLResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/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 *ExperimentalClient) UpdateChatWorkspaceTTL(ctx context.Context, req UpdateChatWorkspaceTTLRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/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 *ExperimentalClient) GetChatRetentionDays(ctx context.Context) (ChatRetentionDaysResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/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 *ExperimentalClient) UpdateChatRetentionDays(ctx context.Context, req UpdateChatRetentionDaysRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/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 *ExperimentalClient) GetChatDebugRetentionDays(ctx context.Context) (ChatDebugRetentionDaysResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/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 *ExperimentalClient) UpdateChatDebugRetentionDays(ctx context.Context, req UpdateChatDebugRetentionDaysRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/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 *ExperimentalClient) GetChatAutoArchiveDays(ctx context.Context) (ChatAutoArchiveDaysResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/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 *ExperimentalClient) UpdateChatAutoArchiveDays(ctx context.Context, req UpdateChatAutoArchiveDaysRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/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 *ExperimentalClient) UpdateUserChatCustomPrompt(ctx context.Context, req UserChatCustomPrompt) (UserChatCustomPrompt, error) {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/user-prompt", req)
if err != nil {
return 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 *ExperimentalClient) GetUserChatCompactionThresholds(ctx context.Context) (UserChatCompactionThresholds, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/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 *ExperimentalClient) UpdateUserChatCompactionThreshold(ctx context.Context, modelConfigID uuid.UUID, req UpdateUserChatCompactionThresholdRequest) (UserChatCompactionThreshold, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/experimental/chats/config/user-compaction-thresholds/%s", modelConfigID), 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 *ExperimentalClient) DeleteUserChatCompactionThreshold(ctx context.Context, modelConfigID uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/config/user-compaction-thresholds/%s", modelConfigID), 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 *ExperimentalClient) CreateChat(ctx context.Context, req CreateChatRequest) (Chat, error) {
res, err := c.Request(ctx, http.MethodPost, "/api/experimental/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 *ExperimentalClient) StreamChat(ctx context.Context, chatID uuid.UUID, opts *StreamChatOptions) (<-chan ChatStreamEvent, io.Closer, error) {
path := fmt.Sprintf("/api/experimental/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 *ExperimentalClient) WatchChats(ctx context.Context) (<-chan ChatWatchEvent, io.Closer, error) {
conn, err := c.Dial(
ctx,
"/api/experimental/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 *ExperimentalClient) GetChatDebugLogging(ctx context.Context) (ChatDebugLoggingAdminSettings, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/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 *ExperimentalClient) UpdateChatDebugLogging(ctx context.Context, req UpdateChatDebugLoggingAllowUsersRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/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 *ExperimentalClient) GetUserChatDebugLogging(ctx context.Context) (UserChatDebugLoggingSettings, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/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 *ExperimentalClient) UpdateUserChatDebugLogging(ctx context.Context, req UpdateUserChatDebugLoggingRequest) error {
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/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 *ExperimentalClient) GetChat(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/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 *ExperimentalClient) RefreshChatContext(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/experimental/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 *ExperimentalClient) GetChatACL(ctx context.Context, chatID uuid.UUID) (ChatACL, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/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 *ExperimentalClient) UpdateChatACL(ctx context.Context, chatID uuid.UUID, req UpdateChatACL) error {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/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 *ExperimentalClient) 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/experimental/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 *ExperimentalClient) 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/experimental/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 *ExperimentalClient) UpdateChat(ctx context.Context, chatID uuid.UUID, req UpdateChatRequest) error {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/chats/%s", chatID), req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// CreateChatMessage adds a message to a chat.
func (c *ExperimentalClient) CreateChatMessage(ctx context.Context, chatID uuid.UUID, req CreateChatMessageRequest) (CreateChatMessageResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/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 *ExperimentalClient) EditChatMessage(
ctx context.Context,
chatID uuid.UUID,
messageID int64,
req EditChatMessageRequest,
) (EditChatMessageResponse, error) {
res, err := c.Request(
ctx,
http.MethodPatch,
fmt.Sprintf("/api/experimental/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 *ExperimentalClient) InterruptChat(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/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 chat.
// The compaction runs asynchronously through the chat worker and
// bypasses the automatic usage threshold; the chat returns to waiting
// once the summary is committed.
func (c *ExperimentalClient) CompactChat(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/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)
}
// 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 *ExperimentalClient) ReconcileInvalidChatState(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/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)
}
// RegenerateChatTitle requests the server to regenerate the chat's
// title using richer conversation context.
func (c *ExperimentalClient) RegenerateChatTitle(ctx context.Context, chatID uuid.UUID) (Chat, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/title/regenerate", 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 *ExperimentalClient) ProposeChatTitle(ctx context.Context, chatID uuid.UUID) (ProposeChatTitleResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/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.
func (c *ExperimentalClient) GetChatDiffContents(ctx context.Context, chatID uuid.UUID) (ChatDiffContents, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/diff", chatID), nil)
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)
}
// UploadChatFile uploads a file for use in chat messages.
func (c *ExperimentalClient) 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/experimental/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)
}
// GetChatFile retrieves a previously uploaded chat file by ID.
func (c *ExperimentalClient) GetChatFile(ctx context.Context, fileID uuid.UUID) ([]byte, string, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/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 *ExperimentalClient) SubmitToolResults(ctx context.Context, chatID uuid.UUID, req SubmitToolResultsRequest) error {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/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 *ExperimentalClient) 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/experimental/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)
}