You've already forked agentic-coding-workflow
feat(chat): http handler + DTO + admin guard for endpoints/models/usage
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ===== Request DTOs =====
|
||||
|
||||
// CreateConversationReq is the HTTP request body for POST /conversations.
|
||||
type CreateConversationReq struct {
|
||||
ModelID uuid.UUID `json:"model_id"`
|
||||
TemplateID *uuid.UUID `json:"template_id,omitempty"`
|
||||
SystemPrompt *string `json:"system_prompt,omitempty"`
|
||||
ProjectID *uuid.UUID `json:"project_id,omitempty"`
|
||||
WorkspaceID *uuid.UUID `json:"workspace_id,omitempty"`
|
||||
IssueID *uuid.UUID `json:"issue_id,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateConversationTitleReq is the HTTP request body for PATCH /conversations/{id}.
|
||||
type UpdateConversationTitleReq struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// SendMessageReq is the HTTP request body for POST /conversations/{id}/messages.
|
||||
type SendMessageReq struct {
|
||||
Content string `json:"content"`
|
||||
AttachmentIDs []uuid.UUID `json:"attachment_ids,omitempty"`
|
||||
}
|
||||
|
||||
// CreateTemplateReq is the HTTP request body for POST /prompt-templates.
|
||||
type CreateTemplateReq struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Scope string `json:"scope"` // "user" | "system"
|
||||
}
|
||||
|
||||
// UpdateTemplateReq is the HTTP request body for PATCH /prompt-templates/{id}.
|
||||
type UpdateTemplateReq struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// CreateEndpointReq is the HTTP request body for POST /admin/llm-endpoints.
|
||||
type CreateEndpointReq struct {
|
||||
Provider string `json:"provider"`
|
||||
DisplayName string `json:"display_name"`
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key"`
|
||||
}
|
||||
|
||||
// UpdateEndpointReq is the HTTP request body for PATCH /admin/llm-endpoints/{id}.
|
||||
type UpdateEndpointReq struct {
|
||||
DisplayName *string `json:"display_name,omitempty"`
|
||||
BaseURL *string `json:"base_url,omitempty"`
|
||||
APIKey *string `json:"api_key,omitempty"`
|
||||
}
|
||||
|
||||
// CreateModelReq is the HTTP request body for POST /admin/llm-models.
|
||||
type CreateModelReq struct {
|
||||
EndpointID uuid.UUID `json:"endpoint_id"`
|
||||
ModelID string `json:"model_id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Capabilities ModelCapabilities `json:"capabilities"`
|
||||
ContextWindow int `json:"context_window"`
|
||||
MaxOutputTokens int `json:"max_output_tokens"`
|
||||
PromptPricePerMillionUSD float64 `json:"prompt_price_per_million_usd"`
|
||||
CompletionPricePerMillionUSD float64 `json:"completion_price_per_million_usd"`
|
||||
ThinkingPricePerMillionUSD float64 `json:"thinking_price_per_million_usd"`
|
||||
IsTitleGenerator bool `json:"is_title_generator"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// UpdateModelReq is the HTTP request body for PATCH /admin/llm-models/{id}.
|
||||
type UpdateModelReq struct {
|
||||
DisplayName *string `json:"display_name,omitempty"`
|
||||
Capabilities *ModelCapabilities `json:"capabilities,omitempty"`
|
||||
ContextWindow *int `json:"context_window,omitempty"`
|
||||
MaxOutputTokens *int `json:"max_output_tokens,omitempty"`
|
||||
PromptPricePerMillionUSD *float64 `json:"prompt_price_per_million_usd,omitempty"`
|
||||
CompletionPricePerMillionUSD *float64 `json:"completion_price_per_million_usd,omitempty"`
|
||||
ThinkingPricePerMillionUSD *float64 `json:"thinking_price_per_million_usd,omitempty"`
|
||||
IsTitleGenerator *bool `json:"is_title_generator,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
SortOrder *int `json:"sort_order,omitempty"`
|
||||
}
|
||||
|
||||
// ===== Response DTOs =====
|
||||
|
||||
// ConversationDTO is the JSON representation of a Conversation.
|
||||
type ConversationDTO struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
ModelID uuid.UUID `json:"model_id"`
|
||||
SystemPrompt string `json:"system_prompt"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
ProjectID *uuid.UUID `json:"project_id,omitempty"`
|
||||
WorkspaceID *uuid.UUID `json:"workspace_id,omitempty"`
|
||||
IssueID *uuid.UUID `json:"issue_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// MessageDTO is the JSON representation of a Message.
|
||||
type MessageDTO struct {
|
||||
ID int64 `json:"id"`
|
||||
ConversationID uuid.UUID `json:"conversation_id"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Thinking *string `json:"thinking,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ErrorMessage *string `json:"error_message,omitempty"`
|
||||
PromptTokens *int `json:"prompt_tokens,omitempty"`
|
||||
CompletionTokens *int `json:"completion_tokens,omitempty"`
|
||||
ThinkingTokens *int `json:"thinking_tokens,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AttachmentDTO is the JSON representation of an Attachment.
|
||||
type AttachmentDTO struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
MimeType string `json:"mime_type"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
SHA256 string `json:"sha256"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// SendMessageResp is the response body for POST /conversations/{id}/messages.
|
||||
type SendMessageResp struct {
|
||||
UserMessageID int64 `json:"user_message_id"`
|
||||
AssistantMessageID int64 `json:"assistant_message_id"`
|
||||
StreamURL string `json:"stream_url"`
|
||||
}
|
||||
|
||||
// TemplateDTO is the JSON representation of a PromptTemplate.
|
||||
type TemplateDTO struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Scope string `json:"scope"`
|
||||
OwnerID *uuid.UUID `json:"owner_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// EndpointDTO is the JSON representation of a LLMEndpoint.
|
||||
// APIKeyEncrypted is intentionally excluded to prevent leaking secrets.
|
||||
type EndpointDTO struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
DisplayName string `json:"display_name"`
|
||||
BaseURL string `json:"base_url"`
|
||||
CreatedBy uuid.UUID `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ModelDTO is the JSON representation of a LLMModel.
|
||||
type ModelDTO struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
EndpointID uuid.UUID `json:"endpoint_id"`
|
||||
ModelID string `json:"model_id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Capabilities ModelCapabilities `json:"capabilities"`
|
||||
ContextWindow int `json:"context_window"`
|
||||
MaxOutputTokens int `json:"max_output_tokens"`
|
||||
PromptPricePerMillionUSD float64 `json:"prompt_price_per_million_usd"`
|
||||
CompletionPricePerMillionUSD float64 `json:"completion_price_per_million_usd"`
|
||||
ThinkingPricePerMillionUSD float64 `json:"thinking_price_per_million_usd"`
|
||||
IsTitleGenerator bool `json:"is_title_generator"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// DayUsageDTO is the per-day usage row returned to both regular users and admins.
|
||||
type DayUsageDTO struct {
|
||||
Day time.Time `json:"day"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
ThinkingTokens int `json:"thinking_tokens"`
|
||||
CostUSD float64 `json:"cost_usd"`
|
||||
}
|
||||
|
||||
// UserUsageDTO is the per-user aggregate usage row returned to admins.
|
||||
type UserUsageDTO struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
ThinkingTokens int `json:"thinking_tokens"`
|
||||
CostUSD float64 `json:"cost_usd"`
|
||||
}
|
||||
|
||||
// ModelUsageDTO is the per-model aggregate usage row returned to admins.
|
||||
type ModelUsageDTO struct {
|
||||
ModelID uuid.UUID `json:"model_id"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
ThinkingTokens int `json:"thinking_tokens"`
|
||||
CostUSD float64 `json:"cost_usd"`
|
||||
}
|
||||
|
||||
// AdminUsageResp is the response body for GET /admin/usage.
|
||||
type AdminUsageResp struct {
|
||||
ByUser []UserUsageDTO `json:"by_user"`
|
||||
ByModel []ModelUsageDTO `json:"by_model"`
|
||||
ByDay []DayUsageDTO `json:"by_day"`
|
||||
}
|
||||
|
||||
// ===== Conversion helpers =====
|
||||
|
||||
func toConversationDTO(c *Conversation) ConversationDTO {
|
||||
return ConversationDTO{
|
||||
ID: c.ID,
|
||||
UserID: c.UserID,
|
||||
ModelID: c.ModelID,
|
||||
SystemPrompt: c.SystemPrompt,
|
||||
Title: c.Title,
|
||||
ProjectID: c.ProjectID,
|
||||
WorkspaceID: c.WorkspaceID,
|
||||
IssueID: c.IssueID,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toMessageDTO(m *Message) MessageDTO {
|
||||
return MessageDTO{
|
||||
ID: m.ID,
|
||||
ConversationID: m.ConversationID,
|
||||
Role: string(m.Role),
|
||||
Content: m.Content,
|
||||
Thinking: m.Thinking,
|
||||
Status: string(m.Status),
|
||||
ErrorMessage: m.ErrorMessage,
|
||||
PromptTokens: m.PromptTokens,
|
||||
CompletionTokens: m.CompletionTokens,
|
||||
ThinkingTokens: m.ThinkingTokens,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toTemplateDTO(t *PromptTemplate) TemplateDTO {
|
||||
return TemplateDTO{
|
||||
ID: t.ID,
|
||||
Name: t.Name,
|
||||
Content: t.Content,
|
||||
Scope: string(t.Scope),
|
||||
OwnerID: t.OwnerID,
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toEndpointDTO(ep *LLMEndpoint) EndpointDTO {
|
||||
return EndpointDTO{
|
||||
ID: ep.ID,
|
||||
Provider: string(ep.Provider),
|
||||
DisplayName: ep.DisplayName,
|
||||
BaseURL: ep.BaseURL,
|
||||
CreatedBy: ep.CreatedBy,
|
||||
CreatedAt: ep.CreatedAt,
|
||||
UpdatedAt: ep.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toModelDTO(m *LLMModel) ModelDTO {
|
||||
return ModelDTO{
|
||||
ID: m.ID,
|
||||
EndpointID: m.EndpointID,
|
||||
ModelID: m.ModelID,
|
||||
DisplayName: m.DisplayName,
|
||||
Capabilities: m.Capabilities,
|
||||
ContextWindow: m.ContextWindow,
|
||||
MaxOutputTokens: m.MaxOutputTokens,
|
||||
PromptPricePerMillionUSD: m.PromptPricePerMillionUSD,
|
||||
CompletionPricePerMillionUSD: m.CompletionPricePerMillionUSD,
|
||||
ThinkingPricePerMillionUSD: m.ThinkingPricePerMillionUSD,
|
||||
IsTitleGenerator: m.IsTitleGenerator,
|
||||
Enabled: m.Enabled,
|
||||
SortOrder: m.SortOrder,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user