mirror of
https://github.com/coder/coder.git
synced 2026-06-03 21:18:24 +00:00
27cbf5474b
The `/chats/{chat}/diff-status` endpoint was redundant because:
- The `Chat` type already has a `DiffStatus` field
- Listing chats already resolves and returns `diff_status`
- The `getChat` endpoint was the only one not resolving it (passing
`nil`)
## Changes
**Backend:**
- `getChat` now calls `resolveChatDiffStatus` and includes the result in
the response
- Removed `getChatDiffStatus` handler, route (`GET /diff-status`), and
SDK method
- Tests updated to use `GetChat` instead of `GetChatDiffStatus`
**Frontend:**
- `AgentDetail.tsx`: uses `chatQuery.data?.diff_status` instead of
separate query
- `RemoteDiffPanel.tsx`: accepts `diffStatus` as a prop instead of
fetching internally
- `AgentsPage.tsx`: `diff_status_change` events now invalidate the chat
query
- Removed `chatDiffStatus` query, `chatDiffStatusKey`, and
`getChatDiffStatus` API method
1413 lines
60 KiB
Go
1413 lines
60 KiB
Go
package codersdk
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/shopspring/decimal"
|
|
|
|
"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"`
|
|
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"`
|
|
LastError *string `json:"last_error"`
|
|
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"`
|
|
}
|
|
|
|
// 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"
|
|
)
|
|
|
|
// 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.
|
|
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"`
|
|
FileID uuid.NullUUID `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"`
|
|
// 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"`
|
|
}
|
|
|
|
// StripInternal removes internal-only fields that must not be
|
|
// sent to API clients. Call before publishing via REST or SSE.
|
|
//
|
|
// Note: ArgsDelta and ResultDelta are intentionally preserved.
|
|
// They are streaming-only fields consumed by the frontend via
|
|
// SSE message_part events (see processStepStream in chatloop).
|
|
func (p *ChatMessagePart) StripInternal() {
|
|
p.ProviderMetadata = nil
|
|
if p.FileID.Valid {
|
|
p.Data = nil
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
func ChatMessageToolResult(toolCallID, toolName string, result json.RawMessage, isError bool) ChatMessagePart {
|
|
return ChatMessagePart{
|
|
Type: ChatMessagePartTypeToolResult,
|
|
ToolCallID: toolCallID,
|
|
ToolName: toolName,
|
|
Result: result,
|
|
IsError: isError,
|
|
}
|
|
}
|
|
|
|
// ChatMessageFile builds a file chat message part.
|
|
func ChatMessageFile(fileID uuid.UUID, mediaType string) ChatMessagePart {
|
|
return ChatMessagePart{
|
|
Type: ChatMessagePartTypeFile,
|
|
FileID: uuid.NullUUID{UUID: fileID, Valid: true},
|
|
MediaType: mediaType,
|
|
}
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// ChatSystemPromptResponse is the response for getting the chat system prompt.
|
|
type ChatSystemPromptResponse struct {
|
|
SystemPrompt string `json:"system_prompt"`
|
|
}
|
|
|
|
// UpdateChatSystemPromptRequest is the request to update the chat system prompt.
|
|
type UpdateChatSystemPromptRequest struct {
|
|
SystemPrompt string `json:"system_prompt"`
|
|
}
|
|
|
|
// UserChatCustomPromptResponse is the response for getting a user's
|
|
// custom chat prompt.
|
|
type UserChatCustomPromptResponse struct {
|
|
CustomPrompt string `json:"custom_prompt"`
|
|
}
|
|
|
|
// UpdateUserChatCustomPromptRequest is the request to update a user's
|
|
// custom chat prompt.
|
|
type UpdateUserChatCustomPromptRequest struct {
|
|
CustomPrompt string `json:"custom_prompt"`
|
|
}
|
|
|
|
// 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" 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"`
|
|
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 output for model distillation or evals" hidden:"true"`
|
|
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" hidden:"true"`
|
|
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"`
|
|
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" 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" description:"Controls the level of reasoning effort" enum:"low,medium,high,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" description:"Restrict web search to these domains (cannot be used with blocked_domains)"`
|
|
BlockedDomains []string `json:"blocked_domains,omitempty" 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"`
|
|
}
|
|
|
|
// ChatModelOpenRouterReasoningOptions configures OpenRouter reasoning behavior.
|
|
type ChatModelOpenRouterReasoningOptions 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:"low,medium,high"`
|
|
}
|
|
|
|
// 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 *ChatModelOpenRouterReasoningOptions `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"`
|
|
}
|
|
|
|
// ChatModelVercelReasoningOptions configures Vercel reasoning behavior.
|
|
type ChatModelVercelReasoningOptions struct {
|
|
Enabled *bool `json:"enabled,omitempty" description:"Whether reasoning is enabled"`
|
|
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"`
|
|
Exclude *bool `json:"exclude,omitempty" description:"Whether to exclude reasoning content from the response"`
|
|
}
|
|
|
|
// 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 *ChatModelVercelReasoningOptions `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"`
|
|
}
|
|
|
|
// 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"
|
|
)
|
|
|
|
// 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 ChatMessageRole `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"`
|
|
}
|
|
|
|
// 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 error message from the failed attempt.
|
|
Error string `json:"error"`
|
|
// RetryingAt is the timestamp when the retry will be attempted.
|
|
RetryingAt time.Time `json:"retrying_at" format:"date-time"`
|
|
}
|
|
|
|
// 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"`
|
|
Retry *ChatStreamRetry `json:"retry,omitempty"`
|
|
QueuedMessages []ChatQueuedMessage `json:"queued_messages,omitempty"`
|
|
}
|
|
|
|
type chatStreamEnvelope struct {
|
|
Type ServerSentEventType `json:"type"`
|
|
Data json.RawMessage `json:"data,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"`
|
|
ByModel []ChatCostModelBreakdown `json:"by_model"`
|
|
ByChat []ChatCostChatBreakdown `json:"by_chat"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// ListChatsOptions are optional parameters for ListChats.
|
|
type ListChatsOptions struct {
|
|
Query string
|
|
Pagination
|
|
}
|
|
|
|
// ListChats returns all chats for the authenticated user.
|
|
func (c *Client) ListChats(ctx context.Context, opts *ListChatsOptions) ([]Chat, error) {
|
|
var reqOpts []RequestOption
|
|
if opts != nil {
|
|
reqOpts = append(reqOpts, opts.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()
|
|
})
|
|
}
|
|
}
|
|
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 *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
|
|
}
|
|
|
|
// 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 *Client) 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 *Client) 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 *Client) 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 *Client) 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
|
|
}
|
|
|
|
// GetUserChatCustomPrompt fetches the user's custom chat prompt.
|
|
func (c *Client) GetUserChatCustomPrompt(ctx context.Context) (UserChatCustomPromptResponse, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/chats/config/user-prompt", nil)
|
|
if err != nil {
|
|
return UserChatCustomPromptResponse{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return UserChatCustomPromptResponse{}, ReadBodyAsError(res)
|
|
}
|
|
var resp UserChatCustomPromptResponse
|
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
}
|
|
|
|
// UpdateUserChatCustomPrompt updates the user's custom chat prompt.
|
|
func (c *Client) UpdateUserChatCustomPrompt(ctx context.Context, req UpdateUserChatCustomPromptRequest) (UserChatCustomPromptResponse, error) {
|
|
res, err := c.Request(ctx, http.MethodPut, "/api/experimental/chats/config/user-prompt", req)
|
|
if err != nil {
|
|
return UserChatCustomPromptResponse{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return UserChatCustomPromptResponse{}, ReadBodyAsError(res)
|
|
}
|
|
var resp UserChatCustomPromptResponse
|
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// StreamChatOptions are optional parameters for StreamChat.
|
|
type StreamChatOptions struct {
|
|
// AfterID limits the initial snapshot to messages created
|
|
// after the given ID. This is useful for relay connections
|
|
// that only need live message_part events and can skip the
|
|
// full message history.
|
|
AfterID *int64
|
|
}
|
|
|
|
// StreamChat streams chat updates in real time.
|
|
//
|
|
// The returned channel includes initial snapshot events first, followed by
|
|
// live updates. Callers must close the returned io.Closer to release the
|
|
// websocket connection when done.
|
|
func (c *Client) StreamChat(ctx context.Context, chatID uuid.UUID, opts *StreamChatOptions) (<-chan ChatStreamEvent, io.Closer, error) {
|
|
path := fmt.Sprintf("/api/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 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 nil
|
|
}), nil
|
|
}
|
|
|
|
// GetChat returns a chat by ID.
|
|
func (c *Client) GetChat(ctx context.Context, chatID uuid.UUID) (Chat, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/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.
|
|
func (c *Client) GetChatMessages(ctx context.Context, chatID uuid.UUID) (ChatMessagesResponse, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/messages", chatID), nil)
|
|
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)
|
|
}
|
|
|
|
func (c *Client) ArchiveChat(ctx context.Context, chatID uuid.UUID) error {
|
|
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/archive", chatID), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusNoContent {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) UnarchiveChat(ctx context.Context, chatID uuid.UUID) error {
|
|
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/chats/%s/unarchive", 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// UploadChatFile uploads a file for use in chat messages.
|
|
func (c *Client) UploadChatFile(ctx context.Context, organizationID uuid.UUID, contentType string, filename string, rd io.Reader) (UploadChatFileResponse, error) {
|
|
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/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 *Client) 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
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|