mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
3545 lines
147 KiB
Go
3545 lines
147 KiB
Go
package codersdk
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/invopop/jsonschema"
|
|
"github.com/shopspring/decimal"
|
|
"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"
|
|
ChatStatusPending ChatStatus = "pending"
|
|
ChatStatusRunning ChatStatus = "running"
|
|
ChatStatusPaused ChatStatus = "paused"
|
|
ChatStatusCompleted ChatStatus = "completed"
|
|
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"`
|
|
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"`
|
|
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"`
|
|
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"`
|
|
// LastInjectedContext holds the most recently persisted
|
|
// injected context parts (AGENTS.md files and skills). It
|
|
// is updated only when context changes, on first workspace
|
|
// attach or agent change.
|
|
LastInjectedContext []ChatMessagePart `json:"last_injected_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"`
|
|
}
|
|
|
|
// 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"
|
|
)
|
|
|
|
// AllChatMessagePartTypes returns all known ChatMessagePartType values.
|
|
func AllChatMessagePartTypes() []ChatMessagePartType {
|
|
return []ChatMessagePartType{
|
|
ChatMessagePartTypeText,
|
|
ChatMessagePartTypeReasoning,
|
|
ChatMessagePartTypeToolCall,
|
|
ChatMessagePartTypeToolResult,
|
|
ChatMessagePartTypeSource,
|
|
ChatMessagePartTypeFile,
|
|
ChatMessagePartTypeFileReference,
|
|
ChatMessagePartTypeContextFile,
|
|
ChatMessagePartTypeSkill,
|
|
}
|
|
}
|
|
|
|
// 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"`
|
|
Signature string `json:"signature,omitempty"`
|
|
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?"`
|
|
// 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"`
|
|
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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// CreateChatMessageResponse is the response from adding a message to a chat.
|
|
type CreateChatMessageResponse struct {
|
|
Message *ChatMessage `json:"message,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.
|
|
// Edits are always synchronous (no queueing), so the message is returned
|
|
// directly.
|
|
type EditChatMessageResponse struct {
|
|
Message ChatMessage `json:"message"`
|
|
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"`
|
|
}
|
|
|
|
// 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"
|
|
)
|
|
|
|
// Valid reports whether the override context is one of the supported values.
|
|
func (c ChatModelOverrideContext) Valid() bool {
|
|
switch c {
|
|
case ChatModelOverrideContextGeneral,
|
|
ChatModelOverrideContextExplore,
|
|
ChatModelOverrideContextTitleGeneration:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllChatModelOverrideContexts returns all supported override contexts.
|
|
func AllChatModelOverrideContexts() []ChatModelOverrideContext {
|
|
return []ChatModelOverrideContext{
|
|
ChatModelOverrideContextGeneral,
|
|
ChatModelOverrideContextExplore,
|
|
ChatModelOverrideContextTitleGeneration,
|
|
}
|
|
}
|
|
|
|
// 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"`
|
|
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"`
|
|
}
|
|
|
|
// 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"`
|
|
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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// ChatDesktopEnabledResponse is the response for getting the desktop setting.
|
|
type ChatDesktopEnabledResponse struct {
|
|
EnableDesktop bool `json:"enable_desktop"`
|
|
}
|
|
|
|
// UpdateChatDesktopEnabledRequest is the request to update the desktop setting.
|
|
type UpdateChatDesktopEnabledRequest struct {
|
|
EnableDesktop bool `json:"enable_desktop"`
|
|
}
|
|
|
|
// 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 toggles the advisor runtime. When false, advisor is not
|
|
// attached to new chats.
|
|
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"`
|
|
}
|
|
|
|
// 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
|
|
|
|
// ChatComputerUseProviderResponse is the response for getting the computer use
|
|
// provider setting.
|
|
type ChatComputerUseProviderResponse struct {
|
|
Provider string `json:"provider"`
|
|
}
|
|
|
|
// UpdateChatComputerUseProviderRequest is the request to update the computer use
|
|
// provider setting.
|
|
type UpdateChatComputerUseProviderRequest struct {
|
|
Provider string `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
|
|
}
|
|
|
|
// ChatTemplateAllowlist is the request and response body for the
|
|
// chat template allowlist configuration endpoint. An empty list
|
|
// means all templates are allowed.
|
|
type ChatTemplateAllowlist struct {
|
|
TemplateIDs []string `json:"template_ids"`
|
|
}
|
|
|
|
// 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"`
|
|
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"`
|
|
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"`
|
|
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"`
|
|
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"`
|
|
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"`
|
|
Provider string `json:"provider"`
|
|
AIProviderID *uuid.UUID `json:"ai_provider_id,omitempty" 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"`
|
|
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"`
|
|
ReasoningEffort *string `json:"reasoning_effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"`
|
|
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"`
|
|
AllowedDomains []string `json:"allowed_domains,omitempty" label:"Web Search: Allowed Domains" description:"Restrict web search to these domains"`
|
|
}
|
|
|
|
// 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"`
|
|
Effort *string `json:"effort,omitempty" label:"Reasoning Effort" description:"Controls the level of reasoning effort" enum:"low,medium,high,xhigh,max"`
|
|
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)"`
|
|
BlockedDomains []string `json:"blocked_domains,omitempty" label:"Web Search: Blocked Domains" description:"Block web search on these domains (cannot be used with allowed_domains)"`
|
|
}
|
|
|
|
// 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"`
|
|
ReasoningEffort *string `json:"reasoning_effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"`
|
|
}
|
|
|
|
// ChatModelReasoningOptions configures reasoning behavior for model
|
|
// providers that support it.
|
|
type ChatModelReasoningOptions struct {
|
|
Enabled *bool `json:"enabled,omitempty" description:"Whether reasoning is enabled"`
|
|
Exclude *bool `json:"exclude,omitempty" description:"Whether to exclude reasoning content from the response"`
|
|
MaxTokens *int64 `json:"max_tokens,omitempty" description:"Maximum number of tokens for reasoning output"`
|
|
Effort *string `json:"effort,omitempty" description:"Controls the level of reasoning effort" enum:"none,minimal,low,medium,high,xhigh"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// ModelCostConfig stores pricing metadata for a chat model.
|
|
type ModelCostConfig struct {
|
|
InputPricePerMillionTokens *decimal.Decimal `json:"input_price_per_million_tokens,omitempty" description:"Input token price in USD per 1M tokens"`
|
|
OutputPricePerMillionTokens *decimal.Decimal `json:"output_price_per_million_tokens,omitempty" description:"Output token price in USD per 1M tokens"`
|
|
CacheReadPricePerMillionTokens *decimal.Decimal `json:"cache_read_price_per_million_tokens,omitempty" description:"Cache read token price in USD per 1M tokens"`
|
|
CacheWritePricePerMillionTokens *decimal.Decimal `json:"cache_write_price_per_million_tokens,omitempty" description:"Cache write or cache creation token price in USD per 1M tokens"`
|
|
}
|
|
|
|
// 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"`
|
|
Cost *ModelCostConfig `json:"cost,omitempty" description:"Optional pricing metadata for this model"`
|
|
ProviderOptions *ChatModelProviderOptions `json:"provider_options,omitempty" description:"Provider-specific option overrides"`
|
|
}
|
|
|
|
// UnmarshalJSON accepts both the current nested cost object and the previous
|
|
// top-level pricing keys so legacy stored model_config JSON continues to load.
|
|
func (c *ChatModelCallConfig) UnmarshalJSON(data []byte) error {
|
|
type chatModelCallConfigAlias ChatModelCallConfig
|
|
aux := struct {
|
|
*chatModelCallConfigAlias
|
|
InputPricePerMillionTokens *decimal.Decimal `json:"input_price_per_million_tokens,omitempty"`
|
|
OutputPricePerMillionTokens *decimal.Decimal `json:"output_price_per_million_tokens,omitempty"`
|
|
CacheReadPricePerMillionTokens *decimal.Decimal `json:"cache_read_price_per_million_tokens,omitempty"`
|
|
CacheWritePricePerMillionTokens *decimal.Decimal `json:"cache_write_price_per_million_tokens,omitempty"`
|
|
}{
|
|
chatModelCallConfigAlias: (*chatModelCallConfigAlias)(c),
|
|
}
|
|
if err := json.Unmarshal(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
|
|
if aux.InputPricePerMillionTokens == nil &&
|
|
aux.OutputPricePerMillionTokens == nil &&
|
|
aux.CacheReadPricePerMillionTokens == nil &&
|
|
aux.CacheWritePricePerMillionTokens == nil {
|
|
return nil
|
|
}
|
|
|
|
if c.Cost == nil {
|
|
c.Cost = &ModelCostConfig{}
|
|
}
|
|
if c.Cost.InputPricePerMillionTokens == nil {
|
|
c.Cost.InputPricePerMillionTokens = aux.InputPricePerMillionTokens
|
|
}
|
|
if c.Cost.OutputPricePerMillionTokens == nil {
|
|
c.Cost.OutputPricePerMillionTokens = aux.OutputPricePerMillionTokens
|
|
}
|
|
if c.Cost.CacheReadPricePerMillionTokens == nil {
|
|
c.Cost.CacheReadPricePerMillionTokens = aux.CacheReadPricePerMillionTokens
|
|
}
|
|
if c.Cost.CacheWritePricePerMillionTokens == nil {
|
|
c.Cost.CacheWritePricePerMillionTokens = aux.CacheWritePricePerMillionTokens
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreateChatModelConfigRequest creates a chat model config.
|
|
type CreateChatModelConfigRequest struct {
|
|
Provider string `json:"provider,omitempty"`
|
|
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 {
|
|
Provider string `json:"provider,omitempty"`
|
|
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."
|
|
ChatGitWatchWorkspaceNoAgentsMessage = "Chat workspace has no agents."
|
|
// 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,
|
|
ChatGitWatchWorkspaceNoAgentsMessage:
|
|
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"
|
|
)
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"
|
|
ChatErrorKindStartupTimeout ChatErrorKind = "startup_timeout"
|
|
ChatErrorKindAuth ChatErrorKind = "auth"
|
|
ChatErrorKindConfig ChatErrorKind = "config"
|
|
ChatErrorKindUsageLimit ChatErrorKind = "usage_limit"
|
|
)
|
|
|
|
// AllChatErrorKinds contains every ChatErrorKind value.
|
|
// Update this when adding new constants above.
|
|
var AllChatErrorKinds = []ChatErrorKind{
|
|
ChatErrorKindGeneric,
|
|
ChatErrorKindOverloaded,
|
|
ChatErrorKindRateLimit,
|
|
ChatErrorKindTimeout,
|
|
ChatErrorKindStartupTimeout,
|
|
ChatErrorKindAuth,
|
|
ChatErrorKindConfig,
|
|
ChatErrorKindUsageLimit,
|
|
}
|
|
|
|
// 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"
|
|
ChatWatchEventKindTitleChange ChatWatchEventKind = "title_change"
|
|
ChatWatchEventKindCreated ChatWatchEventKind = "created"
|
|
ChatWatchEventKindDeleted ChatWatchEventKind = "deleted"
|
|
ChatWatchEventKindDiffStatusChange ChatWatchEventKind = "diff_status_change"
|
|
ChatWatchEventKindActionRequired ChatWatchEventKind = "action_required"
|
|
)
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// ChatCostSummaryOptions are optional query parameters for GetChatCostSummary.
|
|
type ChatCostSummaryOptions struct {
|
|
StartDate time.Time
|
|
EndDate time.Time
|
|
}
|
|
|
|
// ChatCostUsersOptions are optional query parameters for GetChatCostUsers.
|
|
type ChatCostUsersOptions struct {
|
|
StartDate time.Time
|
|
EndDate time.Time
|
|
Username string
|
|
Pagination
|
|
}
|
|
|
|
// ChatCostSummary is the response from the chat cost summary endpoint.
|
|
type ChatCostSummary struct {
|
|
StartDate time.Time `json:"start_date" format:"date-time"`
|
|
EndDate time.Time `json:"end_date" format:"date-time"`
|
|
TotalCostMicros int64 `json:"total_cost_micros"`
|
|
PricedMessageCount int64 `json:"priced_message_count"`
|
|
UnpricedMessageCount int64 `json:"unpriced_message_count"`
|
|
TotalInputTokens int64 `json:"total_input_tokens"`
|
|
TotalOutputTokens int64 `json:"total_output_tokens"`
|
|
TotalCacheReadTokens int64 `json:"total_cache_read_tokens"`
|
|
TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"`
|
|
TotalRuntimeMs int64 `json:"total_runtime_ms"`
|
|
ByModel []ChatCostModelBreakdown `json:"by_model"`
|
|
ByChat []ChatCostChatBreakdown `json:"by_chat"`
|
|
UsageLimit *ChatUsageLimitStatus `json:"usage_limit,omitempty"`
|
|
}
|
|
|
|
// ChatCostModelBreakdown contains per-model cost aggregation.
|
|
type ChatCostModelBreakdown struct {
|
|
ModelConfigID uuid.UUID `json:"model_config_id" format:"uuid"`
|
|
DisplayName string `json:"display_name"`
|
|
Provider string `json:"provider"`
|
|
Model string `json:"model"`
|
|
TotalCostMicros int64 `json:"total_cost_micros"`
|
|
MessageCount int64 `json:"message_count"`
|
|
TotalInputTokens int64 `json:"total_input_tokens"`
|
|
TotalOutputTokens int64 `json:"total_output_tokens"`
|
|
TotalCacheReadTokens int64 `json:"total_cache_read_tokens"`
|
|
TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"`
|
|
TotalRuntimeMs int64 `json:"total_runtime_ms"`
|
|
}
|
|
|
|
// ChatCostChatBreakdown contains per-root-chat cost aggregation.
|
|
type ChatCostChatBreakdown struct {
|
|
RootChatID uuid.UUID `json:"root_chat_id" format:"uuid"`
|
|
ChatTitle string `json:"chat_title"`
|
|
TotalCostMicros int64 `json:"total_cost_micros"`
|
|
MessageCount int64 `json:"message_count"`
|
|
TotalInputTokens int64 `json:"total_input_tokens"`
|
|
TotalOutputTokens int64 `json:"total_output_tokens"`
|
|
TotalCacheReadTokens int64 `json:"total_cache_read_tokens"`
|
|
TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"`
|
|
TotalRuntimeMs int64 `json:"total_runtime_ms"`
|
|
}
|
|
|
|
// ChatCostUserRollup contains per-user cost aggregation for admin views.
|
|
type ChatCostUserRollup struct {
|
|
UserID uuid.UUID `json:"user_id" format:"uuid"`
|
|
Username string `json:"username"`
|
|
Name string `json:"name"`
|
|
AvatarURL string `json:"avatar_url"`
|
|
TotalCostMicros int64 `json:"total_cost_micros"`
|
|
MessageCount int64 `json:"message_count"`
|
|
ChatCount int64 `json:"chat_count"`
|
|
TotalInputTokens int64 `json:"total_input_tokens"`
|
|
TotalOutputTokens int64 `json:"total_output_tokens"`
|
|
TotalCacheReadTokens int64 `json:"total_cache_read_tokens"`
|
|
TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"`
|
|
TotalRuntimeMs int64 `json:"total_runtime_ms"`
|
|
}
|
|
|
|
// ChatCostUsersResponse is the response from the admin chat cost users endpoint.
|
|
type ChatCostUsersResponse struct {
|
|
StartDate time.Time `json:"start_date" format:"date-time"`
|
|
EndDate time.Time `json:"end_date" format:"date-time"`
|
|
Count int64 `json:"count"`
|
|
Users []ChatCostUserRollup `json:"users"`
|
|
}
|
|
|
|
// ChatUsageLimitExceededResponse is the 409 response body returned when a
|
|
// chat operation exceeds the caller's usage limit. The structured fields let
|
|
// frontends render user-friendly spend, limit, and reset information without
|
|
// parsing debug text.
|
|
type ChatUsageLimitExceededResponse struct {
|
|
Response
|
|
SpentMicros int64 `json:"spent_micros"`
|
|
LimitMicros int64 `json:"limit_micros"`
|
|
ResetsAt time.Time `json:"resets_at" format:"date-time"`
|
|
}
|
|
|
|
type chatUsageLimitExceededError struct {
|
|
err *Error
|
|
response ChatUsageLimitExceededResponse
|
|
}
|
|
|
|
func (e *chatUsageLimitExceededError) Error() string {
|
|
if e.err == nil {
|
|
return e.response.Message
|
|
}
|
|
return e.err.Error()
|
|
}
|
|
|
|
func (e *chatUsageLimitExceededError) Unwrap() error {
|
|
return e.err
|
|
}
|
|
|
|
func readBodyAsChatUsageLimitError(res *http.Response) error {
|
|
if res == nil || res.StatusCode != http.StatusConflict {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
rawBody, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
return xerrors.Errorf("read body: %w", err)
|
|
}
|
|
|
|
if mimeErr := ExpectJSONMime(res); mimeErr != nil {
|
|
return readRawBodyAsError(res, rawBody)
|
|
}
|
|
|
|
var payload ChatUsageLimitExceededResponse
|
|
if err := json.NewDecoder(bytes.NewReader(rawBody)).Decode(&payload); err == nil && isChatUsageLimitExceededResponse(payload) {
|
|
return &chatUsageLimitExceededError{
|
|
err: newResponseError(res, payload.Response),
|
|
response: payload,
|
|
}
|
|
}
|
|
|
|
return readRawBodyAsError(res, rawBody)
|
|
}
|
|
|
|
func isChatUsageLimitExceededResponse(resp ChatUsageLimitExceededResponse) bool {
|
|
return resp.Message != "" && !resp.ResetsAt.IsZero()
|
|
}
|
|
|
|
func readRawBodyAsError(res *http.Response, rawBody []byte) error {
|
|
if mimeErr := ExpectJSONMime(res); mimeErr != nil {
|
|
if len(rawBody) > 2048 {
|
|
rawBody = append(rawBody[:2048], []byte("...")...)
|
|
}
|
|
if len(rawBody) == 0 {
|
|
rawBody = []byte("no response body")
|
|
}
|
|
return newResponseError(res, Response{
|
|
Message: mimeErr.Error(),
|
|
Detail: string(rawBody),
|
|
})
|
|
}
|
|
|
|
var response Response
|
|
if err := json.NewDecoder(bytes.NewReader(rawBody)).Decode(&response); err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
return newResponseError(res, Response{Message: "empty response body"})
|
|
}
|
|
return xerrors.Errorf("decode body: %w", err)
|
|
}
|
|
if response.Message == "" {
|
|
if len(rawBody) > 1024 {
|
|
rawBody = append(rawBody[:1024], []byte("...")...)
|
|
}
|
|
response.Message = fmt.Sprintf(
|
|
"unexpected status code %d, response has no message",
|
|
res.StatusCode,
|
|
)
|
|
response.Detail = string(rawBody)
|
|
}
|
|
return newResponseError(res, response)
|
|
}
|
|
|
|
func newResponseError(res *http.Response, response Response) *Error {
|
|
if res == nil {
|
|
return &Error{Response: response}
|
|
}
|
|
|
|
var requestMethod, requestURL string
|
|
if res.Request != nil {
|
|
requestMethod = res.Request.Method
|
|
if res.Request.URL != nil {
|
|
requestURL = res.Request.URL.String()
|
|
}
|
|
}
|
|
|
|
var helpMessage string
|
|
if res.StatusCode == http.StatusUnauthorized {
|
|
helpMessage = "Try logging in using 'coder login'."
|
|
}
|
|
|
|
return &Error{
|
|
Response: response,
|
|
statusCode: res.StatusCode,
|
|
method: requestMethod,
|
|
url: requestURL,
|
|
Helper: helpMessage,
|
|
}
|
|
}
|
|
|
|
// ChatUsageLimitExceededFrom extracts a structured chat usage limit response
|
|
// from an SDK error returned by chat mutation methods.
|
|
func ChatUsageLimitExceededFrom(err error) *ChatUsageLimitExceededResponse {
|
|
var limitErr *chatUsageLimitExceededError
|
|
if !errors.As(err, &limitErr) {
|
|
return nil
|
|
}
|
|
return &limitErr.response
|
|
}
|
|
|
|
// ChatUsageLimitPeriod represents the time window for usage limits.
|
|
type ChatUsageLimitPeriod string
|
|
|
|
const (
|
|
ChatUsageLimitPeriodDay ChatUsageLimitPeriod = "day"
|
|
ChatUsageLimitPeriodWeek ChatUsageLimitPeriod = "week"
|
|
ChatUsageLimitPeriodMonth ChatUsageLimitPeriod = "month"
|
|
)
|
|
|
|
// Valid reports whether p is a supported chat usage limit period.
|
|
func (p ChatUsageLimitPeriod) Valid() bool {
|
|
switch p {
|
|
case ChatUsageLimitPeriodDay, ChatUsageLimitPeriodWeek, ChatUsageLimitPeriodMonth:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ChatUsageLimitConfig is the deployment-wide default usage limit config.
|
|
type ChatUsageLimitConfig struct {
|
|
// Nil in the API means no default limit is set. The DB stores 0 when
|
|
// limiting is disabled.
|
|
SpendLimitMicros *int64 `json:"spend_limit_micros"`
|
|
Period ChatUsageLimitPeriod `json:"period"`
|
|
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
|
}
|
|
|
|
// ChatUsageLimitOverride is a per-user override of the deployment default.
|
|
type ChatUsageLimitOverride struct {
|
|
UserID uuid.UUID `json:"user_id" format:"uuid"`
|
|
Username string `json:"username"`
|
|
Name string `json:"name"`
|
|
AvatarURL string `json:"avatar_url"`
|
|
// Nil in the API means no user override is set. Persisted override rows
|
|
// store positive values.
|
|
SpendLimitMicros *int64 `json:"spend_limit_micros"`
|
|
}
|
|
|
|
// ChatUsageLimitGroupOverride represents a group-scoped spend limit override.
|
|
type ChatUsageLimitGroupOverride struct {
|
|
GroupID uuid.UUID `json:"group_id" format:"uuid"`
|
|
GroupName string `json:"group_name"`
|
|
GroupDisplayName string `json:"group_display_name"`
|
|
GroupAvatarURL string `json:"group_avatar_url"`
|
|
MemberCount int64 `json:"member_count"`
|
|
// Nil in the API means no group override is set. Persisted override rows
|
|
// store positive values.
|
|
SpendLimitMicros *int64 `json:"spend_limit_micros"`
|
|
}
|
|
|
|
// UpsertChatUsageLimitOverrideRequest is the body for creating/updating a
|
|
// per-user usage limit override.
|
|
type UpsertChatUsageLimitOverrideRequest struct {
|
|
SpendLimitMicros int64 `json:"spend_limit_micros"` // Must be greater than 0.
|
|
}
|
|
|
|
// UpdateChatUsageLimitOverrideRequest is kept as a compatibility alias.
|
|
type UpdateChatUsageLimitOverrideRequest = UpsertChatUsageLimitOverrideRequest
|
|
|
|
// UpsertChatUsageLimitGroupOverrideRequest is the request to create or update
|
|
// a group-level spend limit override.
|
|
type UpsertChatUsageLimitGroupOverrideRequest struct {
|
|
SpendLimitMicros int64 `json:"spend_limit_micros"` // Must be greater than 0.
|
|
}
|
|
|
|
// UpdateChatUsageLimitGroupOverrideRequest is kept as a compatibility alias.
|
|
type UpdateChatUsageLimitGroupOverrideRequest = UpsertChatUsageLimitGroupOverrideRequest
|
|
|
|
// ChatUsageLimitStatus represents the current spend status for a user
|
|
// within their active limit period.
|
|
type ChatUsageLimitStatus struct {
|
|
IsLimited bool `json:"is_limited"`
|
|
Period ChatUsageLimitPeriod `json:"period,omitempty"`
|
|
SpendLimitMicros *int64 `json:"spend_limit_micros,omitempty"`
|
|
CurrentSpend int64 `json:"current_spend"`
|
|
PeriodStart time.Time `json:"period_start,omitempty" format:"date-time"`
|
|
PeriodEnd time.Time `json:"period_end,omitempty" format:"date-time"`
|
|
}
|
|
|
|
// ChatUsageLimitConfigResponse is returned from the admin config endpoint
|
|
// and includes the config plus a count of models without pricing.
|
|
type ChatUsageLimitConfigResponse struct {
|
|
ChatUsageLimitConfig
|
|
UnpricedModelCount int64 `json:"unpriced_model_count"`
|
|
Overrides []ChatUsageLimitOverride `json:"overrides"`
|
|
GroupOverrides []ChatUsageLimitGroupOverride `json:"group_overrides"`
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
// ListChatsOptions are optional parameters for ListChats.
|
|
type ListChatsOptions struct {
|
|
Query string
|
|
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())
|
|
if opts.Query != "" {
|
|
reqOpts = append(reqOpts, func(r *http.Request) {
|
|
q := r.URL.Query()
|
|
q.Set("q", opts.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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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
|
|
}
|
|
|
|
// GetChatCostSummary returns an aggregate cost summary for the specified
|
|
// user. Zero-valued StartDate or EndDate fields are omitted from the
|
|
// request, letting the server apply its own defaults (typically the last
|
|
// 30 days).
|
|
func (c *ExperimentalClient) GetChatCostSummary(ctx context.Context, user string, opts ChatCostSummaryOptions) (ChatCostSummary, error) {
|
|
qp := url.Values{}
|
|
if !opts.StartDate.IsZero() {
|
|
qp.Set("start_date", opts.StartDate.Format(time.RFC3339))
|
|
}
|
|
if !opts.EndDate.IsZero() {
|
|
qp.Set("end_date", opts.EndDate.Format(time.RFC3339))
|
|
}
|
|
reqURL := fmt.Sprintf("/api/experimental/chats/cost/%s/summary", user)
|
|
if len(qp) > 0 {
|
|
reqURL += "?" + qp.Encode()
|
|
}
|
|
res, err := c.Request(ctx, http.MethodGet, reqURL, nil)
|
|
if err != nil {
|
|
return ChatCostSummary{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return ChatCostSummary{}, ReadBodyAsError(res)
|
|
}
|
|
var summary ChatCostSummary
|
|
return summary, json.NewDecoder(res.Body).Decode(&summary)
|
|
}
|
|
|
|
// GetChatCostUsers returns a per-user cost rollup for the deployment
|
|
// (admin only). Zero-valued StartDate or EndDate fields are omitted from
|
|
// the request, letting the server apply its own defaults (typically the
|
|
// last 30 days).
|
|
func (c *ExperimentalClient) GetChatCostUsers(ctx context.Context, opts ChatCostUsersOptions) (ChatCostUsersResponse, error) {
|
|
qp := url.Values{}
|
|
if !opts.StartDate.IsZero() {
|
|
qp.Set("start_date", opts.StartDate.Format(time.RFC3339))
|
|
}
|
|
if !opts.EndDate.IsZero() {
|
|
qp.Set("end_date", opts.EndDate.Format(time.RFC3339))
|
|
}
|
|
if opts.Username != "" {
|
|
qp.Set("username", opts.Username)
|
|
}
|
|
if opts.Limit > 0 {
|
|
qp.Set("limit", strconv.Itoa(opts.Limit))
|
|
}
|
|
if opts.Offset > 0 {
|
|
qp.Set("offset", strconv.Itoa(opts.Offset))
|
|
}
|
|
reqURL := "/api/experimental/chats/cost/users"
|
|
if len(qp) > 0 {
|
|
reqURL += "?" + qp.Encode()
|
|
}
|
|
res, err := c.Request(ctx, http.MethodGet, reqURL, nil)
|
|
if err != nil {
|
|
return ChatCostUsersResponse{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return ChatCostUsersResponse{}, ReadBodyAsError(res)
|
|
}
|
|
var resp ChatCostUsersResponse
|
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
}
|
|
|
|
// 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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&resp)
|
|
}
|
|
|
|
// GetChatDesktopEnabled returns the deployment-wide desktop setting.
|
|
func (c *ExperimentalClient) GetChatDesktopEnabled(ctx context.Context) (ChatDesktopEnabledResponse, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/desktop-enabled", nil)
|
|
if err != nil {
|
|
return ChatDesktopEnabledResponse{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return ChatDesktopEnabledResponse{}, ReadBodyAsError(res)
|
|
}
|
|
var resp ChatDesktopEnabledResponse
|
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
}
|
|
|
|
// UpdateChatDesktopEnabled updates the deployment-wide desktop setting.
|
|
func (c *ExperimentalClient) UpdateChatDesktopEnabled(ctx context.Context, req UpdateChatDesktopEnabledRequest) error {
|
|
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/desktop-enabled", req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusNoContent {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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
|
|
}
|
|
|
|
// GetChatTemplateAllowlist returns the deployment-wide chat template allowlist.
|
|
func (c *ExperimentalClient) GetChatTemplateAllowlist(ctx context.Context) (ChatTemplateAllowlist, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/template-allowlist", nil)
|
|
if err != nil {
|
|
return ChatTemplateAllowlist{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return ChatTemplateAllowlist{}, ReadBodyAsError(res)
|
|
}
|
|
var resp ChatTemplateAllowlist
|
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
}
|
|
|
|
// UpdateChatTemplateAllowlist updates the deployment-wide chat template allowlist.
|
|
func (c *ExperimentalClient) UpdateChatTemplateAllowlist(ctx context.Context, req ChatTemplateAllowlist) error {
|
|
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/template-allowlist", 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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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{}, readBodyAsChatUsageLimitError(res)
|
|
}
|
|
defer res.Body.Close()
|
|
var chat Chat
|
|
return chat, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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{}, readBodyAsChatUsageLimitError(res)
|
|
}
|
|
defer res.Body.Close()
|
|
var resp CreateChatMessageResponse
|
|
return resp, json.NewDecoder(res.Body).Decode(&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{}, readBodyAsChatUsageLimitError(res)
|
|
}
|
|
defer res.Body.Close()
|
|
var resp EditChatMessageResponse
|
|
return resp, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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{}, readBodyAsChatUsageLimitError(res)
|
|
}
|
|
var chat Chat
|
|
return chat, json.NewDecoder(res.Body).Decode(&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{}, readBodyAsChatUsageLimitError(res)
|
|
}
|
|
var resp ProposeChatTitleResponse
|
|
return resp, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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
|
|
}
|
|
|
|
// GetChatUsageLimitConfig returns the deployment-wide chat usage limit config.
|
|
func (c *ExperimentalClient) GetChatUsageLimitConfig(ctx context.Context) (ChatUsageLimitConfigResponse, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/usage-limits", nil)
|
|
if err != nil {
|
|
return ChatUsageLimitConfigResponse{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return ChatUsageLimitConfigResponse{}, ReadBodyAsError(res)
|
|
}
|
|
var resp ChatUsageLimitConfigResponse
|
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
}
|
|
|
|
// UpdateChatUsageLimitConfig updates the deployment-wide usage limit config.
|
|
func (c *ExperimentalClient) UpdateChatUsageLimitConfig(ctx context.Context, req ChatUsageLimitConfig) (ChatUsageLimitConfig, error) {
|
|
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/usage-limits", req)
|
|
if err != nil {
|
|
return ChatUsageLimitConfig{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return ChatUsageLimitConfig{}, ReadBodyAsError(res)
|
|
}
|
|
var resp ChatUsageLimitConfig
|
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
}
|
|
|
|
// UpsertChatUsageLimitOverride creates or updates a per-user usage limit override.
|
|
func (c *ExperimentalClient) UpsertChatUsageLimitOverride(ctx context.Context, userID uuid.UUID, req UpsertChatUsageLimitOverrideRequest) (ChatUsageLimitOverride, error) {
|
|
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/experimental/chats/usage-limits/overrides/%s", userID), req)
|
|
if err != nil {
|
|
return ChatUsageLimitOverride{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return ChatUsageLimitOverride{}, ReadBodyAsError(res)
|
|
}
|
|
var resp ChatUsageLimitOverride
|
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
}
|
|
|
|
// UpdateChatUserUsageLimitOverride creates or updates a per-user usage limit override.
|
|
func (c *ExperimentalClient) UpdateChatUserUsageLimitOverride(ctx context.Context, userID uuid.UUID, req UpdateChatUsageLimitOverrideRequest) (ChatUsageLimitOverride, error) {
|
|
return c.UpsertChatUsageLimitOverride(ctx, userID, req)
|
|
}
|
|
|
|
// DeleteChatUsageLimitOverride removes a per-user usage limit override.
|
|
func (c *ExperimentalClient) DeleteChatUsageLimitOverride(ctx context.Context, userID uuid.UUID) error {
|
|
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/usage-limits/overrides/%s", userID), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusNoContent {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteChatUserUsageLimitOverride removes a per-user usage limit override.
|
|
func (c *ExperimentalClient) DeleteChatUserUsageLimitOverride(ctx context.Context, userID uuid.UUID) error {
|
|
return c.DeleteChatUsageLimitOverride(ctx, userID)
|
|
}
|
|
|
|
// UpsertChatUsageLimitGroupOverride creates or updates a group-level
|
|
// spend limit override. EXPERIMENTAL: This API is subject to change.
|
|
func (c *ExperimentalClient) UpsertChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID, req UpsertChatUsageLimitGroupOverrideRequest) (ChatUsageLimitGroupOverride, error) {
|
|
res, err := c.Request(ctx, http.MethodPut,
|
|
fmt.Sprintf("/api/experimental/chats/usage-limits/group-overrides/%s", groupID),
|
|
req,
|
|
)
|
|
if err != nil {
|
|
return ChatUsageLimitGroupOverride{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return ChatUsageLimitGroupOverride{}, ReadBodyAsError(res)
|
|
}
|
|
var override ChatUsageLimitGroupOverride
|
|
return override, json.NewDecoder(res.Body).Decode(&override)
|
|
}
|
|
|
|
// DeleteChatUsageLimitGroupOverride removes a group-level spend limit
|
|
// override. EXPERIMENTAL: This API is subject to change.
|
|
func (c *ExperimentalClient) DeleteChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID) error {
|
|
res, err := c.Request(ctx, http.MethodDelete,
|
|
fmt.Sprintf("/api/experimental/chats/usage-limits/group-overrides/%s", groupID),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusNoContent {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetMyChatUsageLimitStatus returns the current user's chat usage limit status.
|
|
func (c *ExperimentalClient) GetMyChatUsageLimitStatus(ctx context.Context) (ChatUsageLimitStatus, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/usage-limits/status", nil)
|
|
if err != nil {
|
|
return ChatUsageLimitStatus{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return ChatUsageLimitStatus{}, ReadBodyAsError(res)
|
|
}
|
|
var resp ChatUsageLimitStatus
|
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
}
|
|
|
|
// 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, json.NewDecoder(res.Body).Decode(&result)
|
|
}
|
|
|
|
// PRInsightsResponse is the response from the PR insights endpoint.
|
|
type PRInsightsResponse struct {
|
|
Summary PRInsightsSummary `json:"summary"`
|
|
TimeSeries []PRInsightsTimeSeriesEntry `json:"time_series"`
|
|
ByModel []PRInsightsModelBreakdown `json:"by_model"`
|
|
PullRequests []PRInsightsPullRequest `json:"recent_prs"`
|
|
}
|
|
|
|
// PRInsightsSummary contains aggregate PR metrics for a time period,
|
|
// plus the previous period's metrics for trend calculation.
|
|
type PRInsightsSummary struct {
|
|
TotalPRsCreated int64 `json:"total_prs_created"`
|
|
TotalPRsMerged int64 `json:"total_prs_merged"`
|
|
MergeRate float64 `json:"merge_rate"`
|
|
TotalAdditions int64 `json:"total_additions"`
|
|
TotalDeletions int64 `json:"total_deletions"`
|
|
TotalCostMicros int64 `json:"total_cost_micros"`
|
|
CostPerMergedPRMicros int64 `json:"cost_per_merged_pr_micros"`
|
|
ApprovalRate float64 `json:"approval_rate"`
|
|
PrevTotalPRsCreated int64 `json:"prev_total_prs_created"`
|
|
PrevTotalPRsMerged int64 `json:"prev_total_prs_merged"`
|
|
PrevMergeRate float64 `json:"prev_merge_rate"`
|
|
PrevCostPerMergedPRMicros int64 `json:"prev_cost_per_merged_pr_micros"`
|
|
}
|
|
|
|
// PRInsightsTimeSeriesEntry is a single data point in the PR
|
|
// activity time series chart.
|
|
type PRInsightsTimeSeriesEntry struct {
|
|
Date time.Time `json:"date" format:"date-time"`
|
|
PRsCreated int64 `json:"prs_created"`
|
|
PRsMerged int64 `json:"prs_merged"`
|
|
PRsClosed int64 `json:"prs_closed"`
|
|
}
|
|
|
|
// PRInsightsModelBreakdown contains PR metrics for a single model.
|
|
type PRInsightsModelBreakdown struct {
|
|
ModelConfigID uuid.UUID `json:"model_config_id" format:"uuid"`
|
|
DisplayName string `json:"display_name"`
|
|
Provider string `json:"provider"`
|
|
TotalPRs int64 `json:"total_prs"`
|
|
MergedPRs int64 `json:"merged_prs"`
|
|
MergeRate float64 `json:"merge_rate"`
|
|
TotalAdditions int64 `json:"total_additions"`
|
|
TotalDeletions int64 `json:"total_deletions"`
|
|
TotalCostMicros int64 `json:"total_cost_micros"`
|
|
CostPerMergedPRMicros int64 `json:"cost_per_merged_pr_micros"`
|
|
}
|
|
|
|
// PRInsightsPullRequest represents a single PR in the recent PRs
|
|
// table.
|
|
type PRInsightsPullRequest struct {
|
|
ChatID uuid.UUID `json:"chat_id" format:"uuid"`
|
|
PRTitle string `json:"pr_title"`
|
|
PRURL *string `json:"pr_url,omitempty"`
|
|
PRNumber *int32 `json:"pr_number,omitempty"`
|
|
State string `json:"state"`
|
|
Draft bool `json:"draft"`
|
|
Additions int32 `json:"additions"`
|
|
Deletions int32 `json:"deletions"`
|
|
ChangedFiles int32 `json:"changed_files"`
|
|
Commits *int32 `json:"commits,omitempty"`
|
|
Approved *bool `json:"approved,omitempty"`
|
|
ChangesRequested bool `json:"changes_requested"`
|
|
ReviewerCount *int32 `json:"reviewer_count,omitempty"`
|
|
AuthorLogin *string `json:"author_login,omitempty"`
|
|
AuthorAvatarURL *string `json:"author_avatar_url,omitempty"`
|
|
BaseBranch string `json:"base_branch"`
|
|
ModelDisplayName string `json:"model_display_name"`
|
|
CostMicros int64 `json:"cost_micros"`
|
|
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
|
}
|