mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
cb37047dce
Follow-up to #25004. The merged change cycles only through messages
already loaded in the in-memory chat store (page size 50). Long chats
and chats whose oldest turns have rolled out of the page lose access to
their earlier prompts in the composer's up/down arrow cycle. This PR
adds a dedicated server endpoint that returns the full prompt history,
newest first, and rewires the composer to use it.
## What changed
### Endpoint
`GET /api/experimental/chats/{chat}/prompts?limit=N`
```go
type ChatPrompt struct { ID int64; Text string }
type ChatPromptsResponse struct { Prompts []ChatPrompt }
```
- `limit`: `0..2000`. `0` (the default) is treated as the server-side
default of 500; out-of-range values return `400`. Negative values are
rejected by the SDK's `PositiveInt32` parser before reaching the
handler.
- Auth: parent-chat read in `dbauthz`, mirroring
`GetChatMessagesByChatID`.
- The SQL filters `role='user'`, `deleted=false`, `visibility IN
('user','both')`, guards the lateral with `jsonb_typeof(content) =
'array'` so legacy V0 scalar-string rows are silently skipped, then
unrolls `content` JSONB with `WITH ORDINALITY` and concatenates only
`type='text'` parts in original order via `string_agg(... ORDER BY
ordinality)`. Messages whose joined text is whitespace-only are dropped
via `HAVING ... ~ '\S'` so cycling never lands on a blank entry.
### Partial index (migration `000494`)
```sql
CREATE INDEX idx_chat_messages_user_prompts
ON chat_messages (chat_id, id DESC)
WHERE deleted = false
AND role = 'user'
AND visibility IN ('user', 'both');
```
The partial WHERE matches the query's filter exactly and the key order
matches `ORDER BY id DESC`, so the planner gets both the filter and the
ordering from the index without a sort step.
`EXPLAIN ANALYZE` on a synthetic 51-chat × 5,000-message dataset (≈260k
rows, 10k user prompts in the target chat, `random_page_cost=1.1`):
| | Plan | Buffers hit | Time |
|---|---|---|---|
| Without index | `Index Scan Backward using chat_messages_pkey`,
**250,848 rows removed by filter** | 6,683 | 32.4 ms |
| With index | `Index Scan using idx_chat_messages_user_prompts`, no
filter | 38 | 1.3 ms |
≈25× faster, 175× fewer buffer hits.
### Frontend
- `chatPromptsKey` / `chatPromptsQuery` factories in
`site/src/api/queries/chats.ts` (`staleTime: 30s`, `enabled: chatId !==
""`, asks the server for 500 prompts).
- `ChatPageContent.tsx` replaces the in-memory derivation with
`useQuery(chatPromptsQuery(chatId ?? ""))`. The composer's existing
`cycleHistorySnapshotRef` anchors the in-flight cycle so a refetch
arriving mid-cycle cannot shift the indexed prompt out from under the
user.
- `getEditableUserMessagePayload` now concatenates user-message text
parts verbatim, mirroring the server's `string_agg(part->>'text', ''
ORDER BY ordinality)`, instead of routing through the streaming-oriented
`parseMessageContent` / `appendText` pipeline (which drops
whitespace-only chunks — correct for assistant streams, wrong for a
user's persisted message). This keeps the cycle and the edit path in
agreement on the same message. File blocks are still pulled separately
via
`parseMessageContent(...).blocks.filter(isEditableUserMessageFileBlock)`.
- Cache invalidation in `createChatMessage.onSuccess`,
`editChatMessage.onSettled`, and `useChatStore.upsertCacheMessages`
(only when an upserted message has `role === "user"`).
- Page-level stories pre-seed `chatPromptsKey(CHAT_ID)` from the same
`messagesData` to keep them offline.
## Tests
- New `TestGetChatUserPrompts` in `coderd/exp_chats_test.go` with five
subtests:
- `NewestFirstFiltering` — multi-part concatenation, non-text parts
skipped, whitespace-only filtered, soft-deleted excluded, `model`-only
visibility excluded, assistant-role excluded by `cm.role = 'user'`,
legacy V0 scalar row silently excluded by the `jsonb_typeof` guard,
ordering newest first.
- `LimitClampsResults` — explicit `limit=2` returns the two newest
prompts.
- `InvalidLimitRejected` — `limit=5000` is `400 Bad Request`.
- `NotFoundForOtherUsers` — a separate user in the same org gets `404`,
not the prompts.
- `EmptyResultIsJSONArray` — zero-message chat and assistant-only chat
both return `Prompts: []` (non-nil, empty).
- New unit test in `messageParsing.test.ts` asserting that
`getEditableUserMessagePayload(["hello", " ", "world"])` returns `"hello
world"`, locking in the agreement with the SQL `string_agg`.
- `dbauthz_test.go` adds the
`MethodTestSuite.TestChats/GetChatUserPromptsByChatID` entry, asserting
parent-chat `policy.ActionRead`.
- `pnpm test src/pages/AgentsPage` — 1159 passed, 2 skipped.
- `make gen` produces no diff.
## Manual verification
Seeded a dev chat with Claude Sonnet 4.6 via the aibridge Anthropic
provider and posted 20 user prompts end-to-end. Verified that the
`/prompts` endpoint returns 20 rows newest-first, that `limit=10` clamps
correctly, that `limit=0` uses the server default of 500, and that the
up/down keyboard cycle in the composer walks the same sequence (and
reverses correctly back to the empty draft).
## Out of scope
- Cross-chat history.
- Per-user opt-out for the cycle.
- File-reference / attachment cycling — the cycle continues to reproduce
plain text only, by design.
<details>
<summary>Implementation plan</summary>
# CODAGT-319 Follow-up — Dedicated `/prompts` endpoint
## Context
The merged feature ([#25004](https://github.com/coder/coder/pull/25004)
/ [d32842f](https://github.com/coder/coder/commit/d32842f)) cycles only
through messages already loaded in the in-memory chat store, which is
capped at the first 50 messages of the current page. Long chats and
chats whose oldest turns have rolled out of the page can no longer
recall their full prompt history. This follow-up exposes a dedicated
server endpoint that returns the user-authored prompts in a chat, newest
first, and rewires the composer to use it.
## Design
### Endpoint
`GET /api/experimental/chats/{chat}/prompts?limit=N`
Returns:
```go
type ChatPrompt struct {
ID int64
Text string
}
type ChatPromptsResponse struct {
Prompts []ChatPrompt
}
```
- `limit`: `0..2000`. `0` (the default) → server-side default of 500.
The wire-level default is encoded in SQL as `COALESCE(NULLIF($limit, 0),
500)`. Negatives are rejected upstream by `PositiveInt32`; the handler
only caps the upper bound.
- Auth: parent-chat read in `dbauthz`, mirroring
`GetChatMessagesByChatID`.
- Listed under the experimental router so we can iterate without API
guarantees.
### SQL
The query lives in `coderd/database/queries/chats.sql` as
`GetChatUserPromptsByChatID`:
- Filters `role='user'`, `deleted=false`, `visibility IN
('user','both')` to mirror the composer's "what the user actually typed
and can re-send" contract.
- Guards the lateral with `jsonb_typeof(content) = 'array'` so legacy V0
rows whose content is a scalar JSON string (predates migration `000434`)
are silently excluded instead of raising `"cannot extract elements from
a scalar"`.
- Unrolls `content` JSONB with `jsonb_array_elements WITH ORDINALITY`
and concatenates only `type='text'` parts, preserving original order via
`string_agg(... ORDER BY ordinality)`.
- Casts the result to `text` so sqlc emits a `string` field instead of
`[]byte`.
- Drops whitespace-only prompts via `HAVING string_agg(...) ~ '\S'` so
cycling never lands on a blank entry.
- Orders by `cm.id DESC` (`id` is a sequence, so this is "newest first"
without relying on `created_at`).
### Index
New partial index added in migration `000494`:
```sql
CREATE INDEX idx_chat_messages_user_prompts
ON chat_messages (chat_id, id DESC)
WHERE deleted = false
AND role = 'user'
AND visibility IN ('user', 'both');
```
The partial WHERE clause matches the query's filter exactly, so the
planner can use the index for both filtering and ordering without a sort
step.
### Frontend
- `chatPromptsKey(chatId)` and `chatPromptsQuery(chatId)` factories in
`site/src/api/queries/chats.ts`. `staleTime: 30s`, `enabled: chatId !==
""`. Asks the server for 500 prompts (well below the 2000 max, plenty
for the cycle).
- `ChatPageContent.tsx` replaces the in-memory derivation with
`useQuery(chatPromptsQuery(chatId ?? ""))`. The composer's
`cycleHistorySnapshotRef` already takes a stable snapshot at cycle
entry, so a refetch arriving mid-cycle cannot shift the indexed prompt
out from under the user.
- `getEditableUserMessagePayload` extracts the edit-path text from raw
user-message parts (filter `type === "text"`, join verbatim) instead of
going through `parseMessageContent` / `appendText`, which is built for
assistant streams and intentionally drops whitespace-only chunks.
Without this, cycling and clicking Edit on the same message could
produce different draft text for messages with whitespace-only
interleaved text parts.
- Cache invalidation: `createChatMessage.onSuccess`,
`editChatMessage.onSettled`, and `useChatStore.upsertCacheMessages`
(when at least one upserted message has `role === "user"`) all
invalidate `chatPromptsKey(chatId)`.
### Tests
- `TestGetChatUserPrompts` (`coderd/exp_chats_test.go`) covers:
- `NewestFirstFiltering` — multi-part concatenation, non-text parts
skipped, whitespace-only filtered, soft-deleted excluded, `model`-only
visibility excluded, assistant-role excluded by `cm.role = 'user'`,
legacy V0 scalar row silently excluded by the `jsonb_typeof` guard,
ordering newest first.
- `LimitClampsResults` — explicit `limit=2` returns the two newest
prompts.
- `InvalidLimitRejected` — `limit=5000` is `400 Bad Request`.
- `NotFoundForOtherUsers` — a separate user in the same org gets `404`,
not the prompts.
- `EmptyResultIsJSONArray` — zero-message chat and assistant-only chat
both return `Prompts: []` (non-nil, empty).
- `messageParsing.test.ts` adds a unit test asserting that
`getEditableUserMessagePayload(["hello", " ", "world"])` returns `"hello
world"`, locking in the agreement with the SQL `string_agg`.
- `dbauthz_test.go` adds the
`MethodTestSuite.TestChats/GetChatUserPromptsByChatID` entry, asserting
the parent-chat `policy.ActionRead`.
## Out of scope
- Cross-chat history.
- Per-user opt-out for the cycle.
- File-reference / attachment cycling — the cycle still reproduces plain
text only, by design.
</details>
<details>
<summary>coder-agents-review history</summary>
Four review rounds, eight unique findings, all addressed in this PR
(approved twice). Rebased onto `main` twice after R4: first to pick up
new migrations `000491` / `000492`, then again for
`000493_idx_chat_diff_statuses_url_lower`. The prompts-index migration
was renumbered `000491 → 000493 → 000494` via
`coderd/database/migrations/fix_migration_numbers.sh`; no other diff
changes.
| Round | Head | Outcome |
|---|---|---|
| R1 | `725422ab` | `COMMENTED` — 7 findings (DEREM-1..7) |
| R2 | `ab2a8936` | `COMMENTED` — 1 new (DEREM-10) + 1 reraised
(DEREM-5) |
| R3 | `648c5d1f` | **`APPROVED`** — 7 fixed, DEREM-5 deferred via
#25125 |
| R4 | `93b6f450` | **`APPROVED`** — DEREM-5 also fixed in-PR, #25125
closed |
| ID | Where | Resolution |
|---|---|---|
| DEREM-1 | `chats.sql` | Added `jsonb_typeof(content) = 'array'` guard
against V0 scalar rows |
| DEREM-2 | `exp_chats.go` | Removed dead `limit < 0` branch (SDK
rejects upstream) |
| DEREM-3 | `useChatStore.ts` | Rewrote misleading invalidation comment
|
| DEREM-4 | `exp_chats_test.go` | `NewestFirstFiltering` now inserts an
assistant-role message so the `role='user'` filter is exercised
end-to-end |
| DEREM-5 | `messageParsing.ts` | Rewrote
`getEditableUserMessagePayload` to concatenate text parts verbatim,
mirroring the SQL `string_agg` |
| DEREM-6 | `exp_chats.go` | Tightened swagger doc + error message to
spell out the 0–2000 range |
| DEREM-7 | `exp_chats_test.go` | Added `EmptyResultIsJSONArray` subtest
|
| DEREM-10 | `exp_chats_test.go` | `NewestFirstFiltering` now inserts a
raw V0 scalar-content row; verified locally that removing the guard
makes the test fail |
</details>
---
This PR was created on behalf of @ibetitsmike by Coder Agents.
3383 lines
140 KiB
Go
3383 lines
140 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 = 20
|
|
|
|
// 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"
|
|
)
|
|
|
|
// 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"`
|
|
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?"`
|
|
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 records when this part was produced. Present on
|
|
// tool-call and tool-result parts so the frontend can compute
|
|
// tool execution duration.
|
|
CreatedAt *time.Time `json:"created_at,omitempty" format:"date-time" variants:"tool-call?,tool-result?"`
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
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"`
|
|
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"`
|
|
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"`
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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"`
|
|
}
|