mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add experimental agents support (#22290)
feat: add AI chat system with agent tools and chat UI Introduce the chatd subsystem and Agents UI for AI-powered chat within Coder workspaces. - Add chatd package with chat loop, message compaction, prompt management, and LLM provider integration (OpenAI, Anthropic) - Add agent tools: create workspace, list/read templates, read/write/ edit files, execute commands - Add chat API endpoints with streaming, message editing, and durable reconnection - Add database schema and migrations for chats, chat messages, chat providers, and chat model configs - Add RBAC policies and dbauthz enforcement for chat resources - Add Agents UI pages with conversation timeline, queued messages list, diff viewer, and model configuration panel - Add comprehensive test coverage including coderd integration tests, chatd unit tests, and Storybook stories - Gate feature behind experiments flag --------- Co-authored-by: Cian Johnston <cian@coder.com> Co-authored-by: Danielle Maywood <danielle@themaywoods.com> Co-authored-by: Jeremy Ruppel <jeremy@coder.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Cian Johnston
Danielle Maywood
Jeremy Ruppel
Claude Sonnet 4.6
parent
67da4e8b56
commit
edee917d88
@@ -638,6 +638,14 @@ type ExternalAuthRequest struct {
|
||||
ID string
|
||||
// Match is an arbitrary string matched against the regex of the provider.
|
||||
Match string
|
||||
// GitBranch is the current git branch in the working directory.
|
||||
// Sent by the agent so the control plane can resolve diffs
|
||||
// without SSHing into the workspace.
|
||||
GitBranch string
|
||||
// GitRemoteOrigin is the remote origin URL of the git repository.
|
||||
// Sent by the agent so the control plane can resolve diffs
|
||||
// without SSHing into the workspace.
|
||||
GitRemoteOrigin string
|
||||
// Listen indicates that the request should be long-lived and listen for
|
||||
// a new token to be requested.
|
||||
Listen bool
|
||||
@@ -653,6 +661,12 @@ func (c *Client) ExternalAuth(ctx context.Context, req ExternalAuthRequest) (Ext
|
||||
if req.Listen {
|
||||
q.Set("listen", "true")
|
||||
}
|
||||
if req.GitBranch != "" {
|
||||
q.Set("git_branch", req.GitBranch)
|
||||
}
|
||||
if req.GitRemoteOrigin != "" {
|
||||
q.Set("git_remote_origin", req.GitRemoteOrigin)
|
||||
}
|
||||
reqURL := "/api/v2/workspaceagents/me/external-auth?" + q.Encode()
|
||||
res, err := c.SDK.Request(ctx, http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -153,3 +153,33 @@ func TestRewriteDERPMap(t *testing.T) {
|
||||
require.Equal(t, "coconuts.org", node.HostName)
|
||||
require.Equal(t, 44558, node.DERPPort)
|
||||
}
|
||||
|
||||
func TestExternalAuthRequestQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("IncludesGitRefFieldsAndOmitsWorkdir", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/api/v2/workspaceagents/me/external-auth", r.URL.Path)
|
||||
require.Equal(t, "true", r.URL.Query().Get("listen"))
|
||||
require.Equal(t, "main", r.URL.Query().Get("git_branch"))
|
||||
require.Equal(t, "https://github.com/coder/coder.git", r.URL.Query().Get("git_remote_origin"))
|
||||
require.False(t, r.URL.Query().Has("workdir"))
|
||||
_, _ = w.Write([]byte(`{"type":"github","access_token":"token"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
parsedURL, err := url.Parse(srv.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := agentsdk.New(parsedURL, agentsdk.WithFixedToken("token"))
|
||||
_, err = client.ExternalAuth(testutil.Context(t, testutil.WaitShort), agentsdk.ExternalAuthRequest{
|
||||
Match: "github.com",
|
||||
Listen: true,
|
||||
GitBranch: "main",
|
||||
GitRemoteOrigin: "https://github.com/coder/coder.git",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ const (
|
||||
APIKeyScopeBoundaryUsageDelete APIKeyScope = "boundary_usage:delete"
|
||||
APIKeyScopeBoundaryUsageRead APIKeyScope = "boundary_usage:read"
|
||||
APIKeyScopeBoundaryUsageUpdate APIKeyScope = "boundary_usage:update"
|
||||
APIKeyScopeChatAll APIKeyScope = "chat:*"
|
||||
APIKeyScopeChatCreate APIKeyScope = "chat:create"
|
||||
APIKeyScopeChatDelete APIKeyScope = "chat:delete"
|
||||
APIKeyScopeChatRead APIKeyScope = "chat:read"
|
||||
APIKeyScopeChatUpdate APIKeyScope = "chat:update"
|
||||
APIKeyScopeCoderAll APIKeyScope = "coder:all"
|
||||
APIKeyScopeCoderApikeysManageSelf APIKeyScope = "coder:apikeys.manage_self"
|
||||
APIKeyScopeCoderApplicationConnect APIKeyScope = "coder:application_connect"
|
||||
|
||||
@@ -0,0 +1,903 @@
|
||||
package codersdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
)
|
||||
|
||||
// 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"
|
||||
)
|
||||
|
||||
// Chat represents a chat session with an AI agent.
|
||||
type Chat struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
|
||||
WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"`
|
||||
WorkspaceAgentID *uuid.UUID `json:"workspace_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"`
|
||||
DiffStatus *ChatDiffStatus `json:"diff_status,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_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"`
|
||||
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
Role string `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"`
|
||||
}
|
||||
|
||||
// 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"
|
||||
)
|
||||
|
||||
// ChatMessagePart is a structured chunk of a chat message.
|
||||
type ChatMessagePart struct {
|
||||
Type ChatMessagePartType `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolName string `json:"tool_name,omitempty"`
|
||||
Args json.RawMessage `json:"args,omitempty"`
|
||||
ArgsDelta string `json:"args_delta,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
ResultDelta string `json:"result_delta,omitempty"`
|
||||
IsError bool `json:"is_error,omitempty"`
|
||||
SourceID string `json:"source_id,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
MediaType string `json:"media_type,omitempty"`
|
||||
Data []byte `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// ChatInputPartType represents an input part type for user chat input.
|
||||
type ChatInputPartType string
|
||||
|
||||
const (
|
||||
ChatInputPartTypeText ChatInputPartType = "text"
|
||||
)
|
||||
|
||||
// ChatInputPart is a single user input part for creating a chat.
|
||||
type ChatInputPart struct {
|
||||
Type ChatInputPartType `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
// CreateChatRequest is the request to create a new chat.
|
||||
type CreateChatRequest struct {
|
||||
Content []ChatInputPart `json:"content"`
|
||||
WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"`
|
||||
ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"`
|
||||
}
|
||||
|
||||
// UpdateChatRequest is the request to update a chat.
|
||||
type UpdateChatRequest struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// EditChatMessageRequest is the request to edit a user message in a chat.
|
||||
type EditChatMessageRequest struct {
|
||||
Content []ChatInputPart `json:"content"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// ChatWithMessages is a chat along with its messages.
|
||||
type ChatWithMessages struct {
|
||||
Chat Chat `json:"chat"`
|
||||
Messages []ChatMessage `json:"messages"`
|
||||
QueuedMessages []ChatQueuedMessage `json:"queued_messages"`
|
||||
}
|
||||
|
||||
// ChatModelProviderUnavailableReason explains why a provider cannot be used.
|
||||
type ChatModelProviderUnavailableReason string
|
||||
|
||||
const (
|
||||
ChatModelProviderUnavailableMissingAPIKey ChatModelProviderUnavailableReason = "missing_api_key"
|
||||
ChatModelProviderUnavailableFetchFailed ChatModelProviderUnavailableReason = "fetch_failed"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Instructions *string `json:"instructions,omitempty"`
|
||||
LogitBias map[string]int64 `json:"logit_bias,omitempty"`
|
||||
LogProbs *bool `json:"log_probs,omitempty"`
|
||||
TopLogProbs *int64 `json:"top_log_probs,omitempty"`
|
||||
MaxToolCalls *int64 `json:"max_tool_calls,omitempty"`
|
||||
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
|
||||
User *string `json:"user,omitempty"`
|
||||
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
|
||||
ReasoningSummary *string `json:"reasoning_summary,omitempty"`
|
||||
MaxCompletionTokens *int64 `json:"max_completion_tokens,omitempty"`
|
||||
TextVerbosity *string `json:"text_verbosity,omitempty"`
|
||||
Prediction map[string]any `json:"prediction,omitempty"`
|
||||
Store *bool `json:"store,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
PromptCacheKey *string `json:"prompt_cache_key,omitempty"`
|
||||
SafetyIdentifier *string `json:"safety_identifier,omitempty"`
|
||||
ServiceTier *string `json:"service_tier,omitempty"`
|
||||
StructuredOutputs *bool `json:"structured_outputs,omitempty"`
|
||||
StrictJSONSchema *bool `json:"strict_json_schema,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelAnthropicThinkingOptions configures Anthropic thinking budget.
|
||||
type ChatModelAnthropicThinkingOptions struct {
|
||||
BudgetTokens *int64 `json:"budget_tokens,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelAnthropicProviderOptions configures Anthropic provider behavior.
|
||||
type ChatModelAnthropicProviderOptions struct {
|
||||
SendReasoning *bool `json:"send_reasoning,omitempty"`
|
||||
Thinking *ChatModelAnthropicThinkingOptions `json:"thinking,omitempty"`
|
||||
Effort *string `json:"effort,omitempty"`
|
||||
DisableParallelToolUse *bool `json:"disable_parallel_tool_use,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelGoogleThinkingConfig configures Google thinking behavior.
|
||||
type ChatModelGoogleThinkingConfig struct {
|
||||
ThinkingBudget *int64 `json:"thinking_budget,omitempty"`
|
||||
IncludeThoughts *bool `json:"include_thoughts,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelGoogleSafetySetting configures Google safety filtering.
|
||||
type ChatModelGoogleSafetySetting struct {
|
||||
Category string `json:"category,omitempty"`
|
||||
Threshold string `json:"threshold,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelGoogleProviderOptions configures Google provider behavior.
|
||||
type ChatModelGoogleProviderOptions struct {
|
||||
ThinkingConfig *ChatModelGoogleThinkingConfig `json:"thinking_config,omitempty"`
|
||||
CachedContent string `json:"cached_content,omitempty"`
|
||||
SafetySettings []ChatModelGoogleSafetySetting `json:"safety_settings,omitempty"`
|
||||
Threshold string `json:"threshold,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelOpenAICompatProviderOptions configures OpenAI-compatible behavior.
|
||||
type ChatModelOpenAICompatProviderOptions struct {
|
||||
User *string `json:"user,omitempty"`
|
||||
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelOpenRouterReasoningOptions configures OpenRouter reasoning behavior.
|
||||
type ChatModelOpenRouterReasoningOptions struct {
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Exclude *bool `json:"exclude,omitempty"`
|
||||
MaxTokens *int64 `json:"max_tokens,omitempty"`
|
||||
Effort *string `json:"effort,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelOpenRouterProvider configures OpenRouter routing preferences.
|
||||
type ChatModelOpenRouterProvider struct {
|
||||
Order []string `json:"order,omitempty"`
|
||||
AllowFallbacks *bool `json:"allow_fallbacks,omitempty"`
|
||||
RequireParameters *bool `json:"require_parameters,omitempty"`
|
||||
DataCollection *string `json:"data_collection,omitempty"`
|
||||
Only []string `json:"only,omitempty"`
|
||||
Ignore []string `json:"ignore,omitempty"`
|
||||
Quantizations []string `json:"quantizations,omitempty"`
|
||||
Sort *string `json:"sort,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelOpenRouterProviderOptions configures OpenRouter provider behavior.
|
||||
type ChatModelOpenRouterProviderOptions struct {
|
||||
Reasoning *ChatModelOpenRouterReasoningOptions `json:"reasoning,omitempty"`
|
||||
ExtraBody map[string]any `json:"extra_body,omitempty"`
|
||||
IncludeUsage *bool `json:"include_usage,omitempty"`
|
||||
LogitBias map[string]int64 `json:"logit_bias,omitempty"`
|
||||
LogProbs *bool `json:"log_probs,omitempty"`
|
||||
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
|
||||
User *string `json:"user,omitempty"`
|
||||
Provider *ChatModelOpenRouterProvider `json:"provider,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelVercelReasoningOptions configures Vercel reasoning behavior.
|
||||
type ChatModelVercelReasoningOptions struct {
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
MaxTokens *int64 `json:"max_tokens,omitempty"`
|
||||
Effort *string `json:"effort,omitempty"`
|
||||
Exclude *bool `json:"exclude,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelVercelGatewayProviderOptions configures Vercel routing behavior.
|
||||
type ChatModelVercelGatewayProviderOptions struct {
|
||||
Order []string `json:"order,omitempty"`
|
||||
Models []string `json:"models,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelVercelProviderOptions configures Vercel provider behavior.
|
||||
type ChatModelVercelProviderOptions struct {
|
||||
Reasoning *ChatModelVercelReasoningOptions `json:"reasoning,omitempty"`
|
||||
ProviderOptions *ChatModelVercelGatewayProviderOptions `json:"providerOptions,omitempty"`
|
||||
User *string `json:"user,omitempty"`
|
||||
LogitBias map[string]int64 `json:"logit_bias,omitempty"`
|
||||
LogProbs *bool `json:"logprobs,omitempty"`
|
||||
TopLogProbs *int64 `json:"top_logprobs,omitempty"`
|
||||
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
|
||||
ExtraBody map[string]any `json:"extra_body,omitempty"`
|
||||
}
|
||||
|
||||
// ChatModelCallConfig configures per-call model behavior defaults.
|
||||
type ChatModelCallConfig struct {
|
||||
MaxOutputTokens *int64 `json:"max_output_tokens,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
TopP *float64 `json:"top_p,omitempty"`
|
||||
TopK *int64 `json:"top_k,omitempty"`
|
||||
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
|
||||
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"`
|
||||
ProviderOptions *ChatModelProviderOptions `json:"provider_options,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
ChangesRequested bool `json:"changes_requested"`
|
||||
Additions int32 `json:"additions"`
|
||||
Deletions int32 `json:"deletions"`
|
||||
ChangedFiles int32 `json:"changed_files"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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"
|
||||
)
|
||||
|
||||
// ChatQueuedMessage represents a queued message waiting to be processed.
|
||||
type ChatQueuedMessage struct {
|
||||
ID int64 `json:"id"`
|
||||
ChatID uuid.UUID `json:"chat_id" 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 string `json:"role,omitempty"`
|
||||
Part ChatMessagePart `json:"part"`
|
||||
}
|
||||
|
||||
// ChatStreamStatus represents an updated chat status.
|
||||
type ChatStreamStatus struct {
|
||||
Status ChatStatus `json:"status"`
|
||||
}
|
||||
|
||||
// ChatStreamError represents an error event in the stream.
|
||||
type ChatStreamError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// 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 *ChatStreamError `json:"error,omitempty"`
|
||||
QueuedMessages []ChatQueuedMessage `json:"queued_messages,omitempty"`
|
||||
}
|
||||
|
||||
type chatStreamEnvelope struct {
|
||||
Type ServerSentEventType `json:"type"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// ListChats returns all chats for the authenticated user.
|
||||
func (c *Client) ListChats(ctx context.Context) ([]Chat, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats", nil)
|
||||
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 *Client) 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 *Client) 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 *Client) 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 *Client) 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 *Client) 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
|
||||
}
|
||||
|
||||
// ListChatModelConfigs returns admin-managed chat model configs.
|
||||
func (c *Client) 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 *Client) 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 *Client) 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 *Client) 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
|
||||
}
|
||||
|
||||
// CreateChat creates a new chat.
|
||||
func (c *Client) 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
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return Chat{}, ReadBodyAsError(res)
|
||||
}
|
||||
var chat Chat
|
||||
return chat, json.NewDecoder(res.Body).Decode(&chat)
|
||||
}
|
||||
|
||||
// StreamChat streams chat updates in real time.
|
||||
//
|
||||
// The returned channel includes initial snapshot events first, followed by
|
||||
// live updates. Callers must close the returned io.Closer to release the
|
||||
// websocket connection when done.
|
||||
func (c *Client) StreamChat(ctx context.Context, chatID uuid.UUID) (<-chan ChatStreamEvent, io.Closer, error) {
|
||||
conn, err := c.Dial(
|
||||
ctx,
|
||||
fmt.Sprintf("/api/experimental/chats/%s/stream", chatID),
|
||||
&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 envelope chatStreamEnvelope
|
||||
if err := wsjson.Read(streamCtx, conn, &envelope); err != nil {
|
||||
if streamCtx.Err() != nil {
|
||||
return
|
||||
}
|
||||
switch websocket.CloseStatus(err) {
|
||||
case websocket.StatusNormalClosure, websocket.StatusGoingAway:
|
||||
return
|
||||
}
|
||||
_ = send(ChatStreamEvent{
|
||||
Type: ChatStreamEventTypeError,
|
||||
Error: &ChatStreamError{
|
||||
Message: fmt.Sprintf("read chat stream: %v", err),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
switch envelope.Type {
|
||||
case ServerSentEventTypePing:
|
||||
continue
|
||||
case ServerSentEventTypeData:
|
||||
var batch []ChatStreamEvent
|
||||
decodeErr := json.Unmarshal(envelope.Data, &batch)
|
||||
if decodeErr == nil {
|
||||
for _, streamedEvent := range batch {
|
||||
if !send(streamedEvent) {
|
||||
return
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
{
|
||||
_ = send(ChatStreamEvent{
|
||||
Type: ChatStreamEventTypeError,
|
||||
Error: &ChatStreamError{
|
||||
Message: fmt.Sprintf(
|
||||
"decode chat stream event batch: %v",
|
||||
decodeErr,
|
||||
),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
case ServerSentEventTypeError:
|
||||
message := "chat stream returned an error"
|
||||
if len(envelope.Data) > 0 {
|
||||
var response Response
|
||||
if err := json.Unmarshal(envelope.Data, &response); err == nil {
|
||||
message = formatChatStreamResponseError(response)
|
||||
} else {
|
||||
trimmed := strings.TrimSpace(string(envelope.Data))
|
||||
if trimmed != "" {
|
||||
message = trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = send(ChatStreamEvent{
|
||||
Type: ChatStreamEventTypeError,
|
||||
Error: &ChatStreamError{
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
return
|
||||
default:
|
||||
_ = send(ChatStreamEvent{
|
||||
Type: ChatStreamEventTypeError,
|
||||
Error: &ChatStreamError{
|
||||
Message: fmt.Sprintf("unknown chat stream event type %q", envelope.Type),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return events, closeFunc(func() error {
|
||||
streamCancel()
|
||||
return conn.Close(websocket.StatusNormalClosure, "")
|
||||
}), nil
|
||||
}
|
||||
|
||||
// GetChat returns a chat by ID, including its messages.
|
||||
func (c *Client) GetChat(ctx context.Context, chatID uuid.UUID) (ChatWithMessages, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s", chatID), nil)
|
||||
if err != nil {
|
||||
return ChatWithMessages{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return ChatWithMessages{}, ReadBodyAsError(res)
|
||||
}
|
||||
var chat ChatWithMessages
|
||||
return chat, json.NewDecoder(res.Body).Decode(&chat)
|
||||
}
|
||||
|
||||
// DeleteChat deletes a chat by ID.
|
||||
func (c *Client) DeleteChat(ctx context.Context, chatID uuid.UUID) error {
|
||||
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/experimental/chats/%s", chatID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateChatMessage adds a message to a chat.
|
||||
func (c *Client) CreateChatMessage(ctx context.Context, chatID uuid.UUID, req CreateChatMessageRequest) (CreateChatMessageResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/messages", chatID), req)
|
||||
if err != nil {
|
||||
return CreateChatMessageResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return CreateChatMessageResponse{}, ReadBodyAsError(res)
|
||||
}
|
||||
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 *Client) EditChatMessage(
|
||||
ctx context.Context,
|
||||
chatID uuid.UUID,
|
||||
messageID int64,
|
||||
req EditChatMessageRequest,
|
||||
) (ChatMessage, error) {
|
||||
res, err := c.Request(
|
||||
ctx,
|
||||
http.MethodPatch,
|
||||
fmt.Sprintf("/api/experimental/chats/%s/messages/%d", chatID, messageID),
|
||||
req,
|
||||
)
|
||||
if err != nil {
|
||||
return ChatMessage{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return ChatMessage{}, ReadBodyAsError(res)
|
||||
}
|
||||
var message ChatMessage
|
||||
return message, json.NewDecoder(res.Body).Decode(&message)
|
||||
}
|
||||
|
||||
// InterruptChat cancels an in-flight chat run and leaves it waiting.
|
||||
func (c *Client) InterruptChat(ctx context.Context, chatID uuid.UUID) (Chat, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/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)
|
||||
}
|
||||
|
||||
// GetChatGitChanges returns git changes for a chat.
|
||||
func (c *Client) GetChatGitChanges(ctx context.Context, chatID uuid.UUID) ([]ChatGitChange, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/git-changes", chatID), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, ReadBodyAsError(res)
|
||||
}
|
||||
var changes []ChatGitChange
|
||||
return changes, json.NewDecoder(res.Body).Decode(&changes)
|
||||
}
|
||||
|
||||
// GetChatDiffStatus returns cached GitHub pull request diff status for a chat.
|
||||
func (c *Client) GetChatDiffStatus(ctx context.Context, chatID uuid.UUID) (ChatDiffStatus, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/diff-status", chatID), nil)
|
||||
if err != nil {
|
||||
return ChatDiffStatus{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return ChatDiffStatus{}, ReadBodyAsError(res)
|
||||
}
|
||||
var status ChatDiffStatus
|
||||
return status, json.NewDecoder(res.Body).Decode(&status)
|
||||
}
|
||||
|
||||
// GetChatDiffContents returns resolved diff contents for a chat.
|
||||
func (c *Client) 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)
|
||||
}
|
||||
|
||||
func formatChatStreamResponseError(response Response) string {
|
||||
message := strings.TrimSpace(response.Message)
|
||||
detail := strings.TrimSpace(response.Detail)
|
||||
switch {
|
||||
case message == "" && detail == "":
|
||||
return "chat stream returned an error"
|
||||
case message == "":
|
||||
return detail
|
||||
case detail == "":
|
||||
return message
|
||||
default:
|
||||
return fmt.Sprintf("%s: %s", message, detail)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package codersdk_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func TestChatModelProviderOptions_MarshalJSON_UsesPlainProviderPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sendReasoning := true
|
||||
effort := "high"
|
||||
|
||||
raw, err := json.Marshal(codersdk.ChatModelProviderOptions{
|
||||
Anthropic: &codersdk.ChatModelAnthropicProviderOptions{
|
||||
SendReasoning: &sendReasoning,
|
||||
Effort: &effort,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, string(raw), `"type":"anthropic.options"`)
|
||||
require.NotContains(t, string(raw), `"data":`)
|
||||
require.Contains(t, string(raw), `"send_reasoning":true`)
|
||||
require.Contains(t, string(raw), `"effort":"high"`)
|
||||
}
|
||||
|
||||
func TestChatModelProviderOptions_UnmarshalJSON_ParsesPlainProviderPayloads(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw := []byte(`{
|
||||
"anthropic": {
|
||||
"send_reasoning": true,
|
||||
"effort": "high"
|
||||
}
|
||||
}`)
|
||||
|
||||
var decoded codersdk.ChatModelProviderOptions
|
||||
err := json.Unmarshal(raw, &decoded)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, decoded.Anthropic)
|
||||
require.NotNil(t, decoded.Anthropic.SendReasoning)
|
||||
require.True(t, *decoded.Anthropic.SendReasoning)
|
||||
require.NotNil(t, decoded.Anthropic.Effort)
|
||||
require.Equal(
|
||||
t,
|
||||
"high",
|
||||
*decoded.Anthropic.Effort,
|
||||
)
|
||||
}
|
||||
+77
-63
@@ -579,68 +579,69 @@ type DeploymentValues struct {
|
||||
DocsURL serpent.URL `json:"docs_url,omitempty"`
|
||||
RedirectToAccessURL serpent.Bool `json:"redirect_to_access_url,omitempty"`
|
||||
// HTTPAddress is a string because it may be set to zero to disable.
|
||||
HTTPAddress serpent.String `json:"http_address,omitempty" typescript:",notnull"`
|
||||
AutobuildPollInterval serpent.Duration `json:"autobuild_poll_interval,omitempty"`
|
||||
JobReaperDetectorInterval serpent.Duration `json:"job_hang_detector_interval,omitempty"`
|
||||
DERP DERP `json:"derp,omitempty" typescript:",notnull"`
|
||||
Prometheus PrometheusConfig `json:"prometheus,omitempty" typescript:",notnull"`
|
||||
Pprof PprofConfig `json:"pprof,omitempty" typescript:",notnull"`
|
||||
ProxyTrustedHeaders serpent.StringArray `json:"proxy_trusted_headers,omitempty" typescript:",notnull"`
|
||||
ProxyTrustedOrigins serpent.StringArray `json:"proxy_trusted_origins,omitempty" typescript:",notnull"`
|
||||
CacheDir serpent.String `json:"cache_directory,omitempty" typescript:",notnull"`
|
||||
EphemeralDeployment serpent.Bool `json:"ephemeral_deployment,omitempty" typescript:",notnull"`
|
||||
PostgresURL serpent.String `json:"pg_connection_url,omitempty" typescript:",notnull"`
|
||||
PostgresAuth string `json:"pg_auth,omitempty" typescript:",notnull"`
|
||||
PostgresConnMaxOpen serpent.Int64 `json:"pg_conn_max_open,omitempty" typescript:",notnull"`
|
||||
PostgresConnMaxIdle serpent.String `json:"pg_conn_max_idle,omitempty" typescript:",notnull"`
|
||||
OAuth2 OAuth2Config `json:"oauth2,omitempty" typescript:",notnull"`
|
||||
OIDC OIDCConfig `json:"oidc,omitempty" typescript:",notnull"`
|
||||
Telemetry TelemetryConfig `json:"telemetry,omitempty" typescript:",notnull"`
|
||||
TLS TLSConfig `json:"tls,omitempty" typescript:",notnull"`
|
||||
Trace TraceConfig `json:"trace,omitempty" typescript:",notnull"`
|
||||
HTTPCookies HTTPCookieConfig `json:"http_cookies,omitempty" typescript:",notnull"`
|
||||
StrictTransportSecurity serpent.Int64 `json:"strict_transport_security,omitempty" typescript:",notnull"`
|
||||
StrictTransportSecurityOptions serpent.StringArray `json:"strict_transport_security_options,omitempty" typescript:",notnull"`
|
||||
SSHKeygenAlgorithm serpent.String `json:"ssh_keygen_algorithm,omitempty" typescript:",notnull"`
|
||||
MetricsCacheRefreshInterval serpent.Duration `json:"metrics_cache_refresh_interval,omitempty" typescript:",notnull"`
|
||||
AgentStatRefreshInterval serpent.Duration `json:"agent_stat_refresh_interval,omitempty" typescript:",notnull"`
|
||||
AgentFallbackTroubleshootingURL serpent.URL `json:"agent_fallback_troubleshooting_url,omitempty" typescript:",notnull"`
|
||||
BrowserOnly serpent.Bool `json:"browser_only,omitempty" typescript:",notnull"`
|
||||
SCIMAPIKey serpent.String `json:"scim_api_key,omitempty" typescript:",notnull"`
|
||||
ExternalTokenEncryptionKeys serpent.StringArray `json:"external_token_encryption_keys,omitempty" typescript:",notnull"`
|
||||
Provisioner ProvisionerConfig `json:"provisioner,omitempty" typescript:",notnull"`
|
||||
RateLimit RateLimitConfig `json:"rate_limit,omitempty" typescript:",notnull"`
|
||||
Experiments serpent.StringArray `json:"experiments,omitempty" typescript:",notnull"`
|
||||
UpdateCheck serpent.Bool `json:"update_check,omitempty" typescript:",notnull"`
|
||||
Swagger SwaggerConfig `json:"swagger,omitempty" typescript:",notnull"`
|
||||
Logging LoggingConfig `json:"logging,omitempty" typescript:",notnull"`
|
||||
Dangerous DangerousConfig `json:"dangerous,omitempty" typescript:",notnull"`
|
||||
DisablePathApps serpent.Bool `json:"disable_path_apps,omitempty" typescript:",notnull"`
|
||||
Sessions SessionLifetime `json:"session_lifetime,omitempty" typescript:",notnull"`
|
||||
DisablePasswordAuth serpent.Bool `json:"disable_password_auth,omitempty" typescript:",notnull"`
|
||||
Support SupportConfig `json:"support,omitempty" typescript:",notnull"`
|
||||
EnableAuthzRecording serpent.Bool `json:"enable_authz_recording,omitempty" typescript:",notnull"`
|
||||
ExternalAuthConfigs serpent.Struct[[]ExternalAuthConfig] `json:"external_auth,omitempty" typescript:",notnull"`
|
||||
SSHConfig SSHConfig `json:"config_ssh,omitempty" typescript:",notnull"`
|
||||
WgtunnelHost serpent.String `json:"wgtunnel_host,omitempty" typescript:",notnull"`
|
||||
DisableOwnerWorkspaceExec serpent.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"`
|
||||
DisableWorkspaceSharing serpent.Bool `json:"disable_workspace_sharing,omitempty" typescript:",notnull"`
|
||||
ProxyHealthStatusInterval serpent.Duration `json:"proxy_health_status_interval,omitempty" typescript:",notnull"`
|
||||
EnableTerraformDebugMode serpent.Bool `json:"enable_terraform_debug_mode,omitempty" typescript:",notnull"`
|
||||
UserQuietHoursSchedule UserQuietHoursScheduleConfig `json:"user_quiet_hours_schedule,omitempty" typescript:",notnull"`
|
||||
WebTerminalRenderer serpent.String `json:"web_terminal_renderer,omitempty" typescript:",notnull"`
|
||||
AllowWorkspaceRenames serpent.Bool `json:"allow_workspace_renames,omitempty" typescript:",notnull"`
|
||||
Healthcheck HealthcheckConfig `json:"healthcheck,omitempty" typescript:",notnull"`
|
||||
Retention RetentionConfig `json:"retention,omitempty" typescript:",notnull"`
|
||||
CLIUpgradeMessage serpent.String `json:"cli_upgrade_message,omitempty" typescript:",notnull"`
|
||||
TermsOfServiceURL serpent.String `json:"terms_of_service_url,omitempty" typescript:",notnull"`
|
||||
Notifications NotificationsConfig `json:"notifications,omitempty" typescript:",notnull"`
|
||||
AdditionalCSPPolicy serpent.StringArray `json:"additional_csp_policy,omitempty" typescript:",notnull"`
|
||||
WorkspaceHostnameSuffix serpent.String `json:"workspace_hostname_suffix,omitempty" typescript:",notnull"`
|
||||
Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"`
|
||||
HideAITasks serpent.Bool `json:"hide_ai_tasks,omitempty" typescript:",notnull"`
|
||||
AI AIConfig `json:"ai,omitempty"`
|
||||
StatsCollection StatsCollectionConfig `json:"stats_collection,omitempty" typescript:",notnull"`
|
||||
HTTPAddress serpent.String `json:"http_address,omitempty" typescript:",notnull"`
|
||||
AutobuildPollInterval serpent.Duration `json:"autobuild_poll_interval,omitempty"`
|
||||
JobReaperDetectorInterval serpent.Duration `json:"job_hang_detector_interval,omitempty"`
|
||||
DERP DERP `json:"derp,omitempty" typescript:",notnull"`
|
||||
Prometheus PrometheusConfig `json:"prometheus,omitempty" typescript:",notnull"`
|
||||
Pprof PprofConfig `json:"pprof,omitempty" typescript:",notnull"`
|
||||
ProxyTrustedHeaders serpent.StringArray `json:"proxy_trusted_headers,omitempty" typescript:",notnull"`
|
||||
ProxyTrustedOrigins serpent.StringArray `json:"proxy_trusted_origins,omitempty" typescript:",notnull"`
|
||||
CacheDir serpent.String `json:"cache_directory,omitempty" typescript:",notnull"`
|
||||
EphemeralDeployment serpent.Bool `json:"ephemeral_deployment,omitempty" typescript:",notnull"`
|
||||
PostgresURL serpent.String `json:"pg_connection_url,omitempty" typescript:",notnull"`
|
||||
PostgresAuth string `json:"pg_auth,omitempty" typescript:",notnull"`
|
||||
PostgresConnMaxOpen serpent.Int64 `json:"pg_conn_max_open,omitempty" typescript:",notnull"`
|
||||
PostgresConnMaxIdle serpent.String `json:"pg_conn_max_idle,omitempty" typescript:",notnull"`
|
||||
OAuth2 OAuth2Config `json:"oauth2,omitempty" typescript:",notnull"`
|
||||
OIDC OIDCConfig `json:"oidc,omitempty" typescript:",notnull"`
|
||||
Telemetry TelemetryConfig `json:"telemetry,omitempty" typescript:",notnull"`
|
||||
TLS TLSConfig `json:"tls,omitempty" typescript:",notnull"`
|
||||
Trace TraceConfig `json:"trace,omitempty" typescript:",notnull"`
|
||||
HTTPCookies HTTPCookieConfig `json:"http_cookies,omitempty" typescript:",notnull"`
|
||||
StrictTransportSecurity serpent.Int64 `json:"strict_transport_security,omitempty" typescript:",notnull"`
|
||||
StrictTransportSecurityOptions serpent.StringArray `json:"strict_transport_security_options,omitempty" typescript:",notnull"`
|
||||
SSHKeygenAlgorithm serpent.String `json:"ssh_keygen_algorithm,omitempty" typescript:",notnull"`
|
||||
MetricsCacheRefreshInterval serpent.Duration `json:"metrics_cache_refresh_interval,omitempty" typescript:",notnull"`
|
||||
AgentStatRefreshInterval serpent.Duration `json:"agent_stat_refresh_interval,omitempty" typescript:",notnull"`
|
||||
AgentFallbackTroubleshootingURL serpent.URL `json:"agent_fallback_troubleshooting_url,omitempty" typescript:",notnull"`
|
||||
BrowserOnly serpent.Bool `json:"browser_only,omitempty" typescript:",notnull"`
|
||||
SCIMAPIKey serpent.String `json:"scim_api_key,omitempty" typescript:",notnull"`
|
||||
ExternalTokenEncryptionKeys serpent.StringArray `json:"external_token_encryption_keys,omitempty" typescript:",notnull"`
|
||||
Provisioner ProvisionerConfig `json:"provisioner,omitempty" typescript:",notnull"`
|
||||
RateLimit RateLimitConfig `json:"rate_limit,omitempty" typescript:",notnull"`
|
||||
Experiments serpent.StringArray `json:"experiments,omitempty" typescript:",notnull"`
|
||||
UpdateCheck serpent.Bool `json:"update_check,omitempty" typescript:",notnull"`
|
||||
Swagger SwaggerConfig `json:"swagger,omitempty" typescript:",notnull"`
|
||||
Logging LoggingConfig `json:"logging,omitempty" typescript:",notnull"`
|
||||
Dangerous DangerousConfig `json:"dangerous,omitempty" typescript:",notnull"`
|
||||
DisablePathApps serpent.Bool `json:"disable_path_apps,omitempty" typescript:",notnull"`
|
||||
Sessions SessionLifetime `json:"session_lifetime,omitempty" typescript:",notnull"`
|
||||
DisablePasswordAuth serpent.Bool `json:"disable_password_auth,omitempty" typescript:",notnull"`
|
||||
Support SupportConfig `json:"support,omitempty" typescript:",notnull"`
|
||||
EnableAuthzRecording serpent.Bool `json:"enable_authz_recording,omitempty" typescript:",notnull"`
|
||||
ExternalAuthConfigs serpent.Struct[[]ExternalAuthConfig] `json:"external_auth,omitempty" typescript:",notnull"`
|
||||
ExternalAuthGithubDefaultProviderEnable serpent.Bool `json:"external_auth_github_default_provider_enable,omitempty" typescript:",notnull"`
|
||||
SSHConfig SSHConfig `json:"config_ssh,omitempty" typescript:",notnull"`
|
||||
WgtunnelHost serpent.String `json:"wgtunnel_host,omitempty" typescript:",notnull"`
|
||||
DisableOwnerWorkspaceExec serpent.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"`
|
||||
DisableWorkspaceSharing serpent.Bool `json:"disable_workspace_sharing,omitempty" typescript:",notnull"`
|
||||
ProxyHealthStatusInterval serpent.Duration `json:"proxy_health_status_interval,omitempty" typescript:",notnull"`
|
||||
EnableTerraformDebugMode serpent.Bool `json:"enable_terraform_debug_mode,omitempty" typescript:",notnull"`
|
||||
UserQuietHoursSchedule UserQuietHoursScheduleConfig `json:"user_quiet_hours_schedule,omitempty" typescript:",notnull"`
|
||||
WebTerminalRenderer serpent.String `json:"web_terminal_renderer,omitempty" typescript:",notnull"`
|
||||
AllowWorkspaceRenames serpent.Bool `json:"allow_workspace_renames,omitempty" typescript:",notnull"`
|
||||
Healthcheck HealthcheckConfig `json:"healthcheck,omitempty" typescript:",notnull"`
|
||||
Retention RetentionConfig `json:"retention,omitempty" typescript:",notnull"`
|
||||
CLIUpgradeMessage serpent.String `json:"cli_upgrade_message,omitempty" typescript:",notnull"`
|
||||
TermsOfServiceURL serpent.String `json:"terms_of_service_url,omitempty" typescript:",notnull"`
|
||||
Notifications NotificationsConfig `json:"notifications,omitempty" typescript:",notnull"`
|
||||
AdditionalCSPPolicy serpent.StringArray `json:"additional_csp_policy,omitempty" typescript:",notnull"`
|
||||
WorkspaceHostnameSuffix serpent.String `json:"workspace_hostname_suffix,omitempty" typescript:",notnull"`
|
||||
Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"`
|
||||
HideAITasks serpent.Bool `json:"hide_ai_tasks,omitempty" typescript:",notnull"`
|
||||
AI AIConfig `json:"ai,omitempty"`
|
||||
StatsCollection StatsCollectionConfig `json:"stats_collection,omitempty" typescript:",notnull"`
|
||||
|
||||
Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
|
||||
WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"`
|
||||
@@ -3153,6 +3154,15 @@ Write out the current server config as YAML to stdout.`,
|
||||
Value: &c.ExternalAuthConfigs,
|
||||
Hidden: true,
|
||||
},
|
||||
{
|
||||
Name: "External Auth GitHub Default Provider Enable",
|
||||
Description: "Enable the default GitHub external auth provider managed by Coder.",
|
||||
Flag: "external-auth-github-default-provider-enable",
|
||||
Env: "CODER_EXTERNAL_AUTH_GITHUB_DEFAULT_PROVIDER_ENABLE",
|
||||
YAML: "externalAuthGithubDefaultProviderEnable",
|
||||
Value: &c.ExternalAuthGithubDefaultProviderEnable,
|
||||
Default: "true",
|
||||
},
|
||||
{
|
||||
Name: "Custom wgtunnel Host",
|
||||
Description: `Hostname of HTTPS server that runs https://github.com/coder/wgtunnel. By default, this will pick the best available wgtunnel server hosted by Coder. e.g. "tunnel.example.com".`,
|
||||
@@ -3583,7 +3593,6 @@ Write out the current server config as YAML to stdout.`,
|
||||
Group: &deploymentGroupClient,
|
||||
YAML: "hideAITasks",
|
||||
},
|
||||
|
||||
// AI Bridge Options
|
||||
{
|
||||
Name: "AI Bridge Enabled",
|
||||
@@ -4264,6 +4273,7 @@ const (
|
||||
ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking.
|
||||
ExperimentWebPush Experiment = "web-push" // Enables web push notifications through the browser.
|
||||
ExperimentOAuth2 Experiment = "oauth2" // Enables OAuth2 provider functionality.
|
||||
ExperimentAgents Experiment = "agents" // Enables agent-powered chat functionality.
|
||||
ExperimentMCPServerHTTP Experiment = "mcp-server-http" // Enables the MCP HTTP server functionality.
|
||||
)
|
||||
|
||||
@@ -4281,6 +4291,8 @@ func (e Experiment) DisplayName() string {
|
||||
return "Browser Push Notifications"
|
||||
case ExperimentOAuth2:
|
||||
return "OAuth2 Provider Functionality"
|
||||
case ExperimentAgents:
|
||||
return "Agents"
|
||||
case ExperimentMCPServerHTTP:
|
||||
return "MCP HTTP Server Functionality"
|
||||
default:
|
||||
@@ -4299,6 +4311,7 @@ var ExperimentsKnown = Experiments{
|
||||
ExperimentWorkspaceUsage,
|
||||
ExperimentWebPush,
|
||||
ExperimentOAuth2,
|
||||
ExperimentAgents,
|
||||
ExperimentMCPServerHTTP,
|
||||
}
|
||||
|
||||
@@ -4306,6 +4319,7 @@ var ExperimentsKnown = Experiments{
|
||||
// users to opt-in to via --experimental='*'.
|
||||
// Experiments that are not ready for consumption by all users should
|
||||
// not be included here and will be essentially hidden.
|
||||
// TODO: Add ExperimentAgents to ExperimentsSafe once it is safe for general use.
|
||||
var ExperimentsSafe = Experiments{}
|
||||
|
||||
// Experiments is a list of experiments.
|
||||
|
||||
@@ -11,6 +11,7 @@ const (
|
||||
ResourceAssignRole RBACResource = "assign_role"
|
||||
ResourceAuditLog RBACResource = "audit_log"
|
||||
ResourceBoundaryUsage RBACResource = "boundary_usage"
|
||||
ResourceChat RBACResource = "chat"
|
||||
ResourceConnectionLog RBACResource = "connection_log"
|
||||
ResourceCryptoKey RBACResource = "crypto_key"
|
||||
ResourceDebugInfo RBACResource = "debug_info"
|
||||
@@ -82,6 +83,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{
|
||||
ResourceAssignRole: {ActionAssign, ActionRead, ActionUnassign},
|
||||
ResourceAuditLog: {ActionCreate, ActionRead},
|
||||
ResourceBoundaryUsage: {ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceChat: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceConnectionLog: {ActionRead, ActionUpdate},
|
||||
ResourceCryptoKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceDebugInfo: {ActionRead},
|
||||
|
||||
Reference in New Issue
Block a user