feat(copilot): finish configuration center
This commit is contained in:
@@ -146,6 +146,28 @@ func TestChatwootAuthValidateTokenReturnsPayloadData(t *testing.T) {
|
||||
assertChatwootAuthUserFixture(t, data)
|
||||
}
|
||||
|
||||
func TestChatwootAuthValidateTokenSerializesPlatformAdminType(t *testing.T) {
|
||||
router, db, user := setupChatwootAuthTest(t)
|
||||
require.NoError(t, db.Model(user).Updates(map[string]any{
|
||||
"role": "super_admin",
|
||||
"type": "User",
|
||||
}).Error)
|
||||
|
||||
token := signInAndReturnAccessToken(t, router)
|
||||
req, _ := http.NewRequest(http.MethodGet, "/auth/validate_token", nil)
|
||||
req.Header.Set("access-token", token)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
var body map[string]any
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
|
||||
data := body["payload"].(map[string]any)["data"].(map[string]any)
|
||||
require.Equal(t, "SuperAdmin", data["type"])
|
||||
require.Equal(t, "administrator", data["role"])
|
||||
}
|
||||
|
||||
func TestChatwootAuthSignOutRevokesRefreshSession(t *testing.T) {
|
||||
router, _, _ := setupChatwootAuthTest(t)
|
||||
token := signInAndReturnAccessToken(t, router)
|
||||
|
||||
@@ -78,7 +78,7 @@ func (s *AutoReplyRuleHandlerTestSuite) SetupSuite() {
|
||||
s.router = r
|
||||
|
||||
// Register routes matching the handler's expected URL patterns
|
||||
accountsGroup := r.Group("/api/v1/accounts/:id/captain")
|
||||
accountsGroup := r.Group("/api/v1/accounts/:account_id/captain")
|
||||
{
|
||||
accountsGroup.POST("/assistants/:assistant_id/auto_reply_rules", s.handler.Create)
|
||||
accountsGroup.GET("/auto_reply_rules/:rule_id", s.handler.Get)
|
||||
@@ -493,11 +493,8 @@ func (s *AutoReplyRuleHandlerTestSuite) TestEvaluate_Success() {
|
||||
}
|
||||
s.Require().NoError(s.db.Create(rule).Error)
|
||||
|
||||
// Note: The handler sets accountID from path param but assigns to _ (not used in evalCtx).
|
||||
// AutoReplyEvaluationContext.AccountID has no json tag, so it stays 0 from JSON binding.
|
||||
// The service EvaluateRules will use AccountID=0 for FindActiveByInbox.
|
||||
// This means even with a valid account in the path, the evaluate endpoint currently
|
||||
// searches by account_id=0. We test the handler behavior as-is.
|
||||
// The account path parameter is authoritative and is copied into the evaluation
|
||||
// context before the service searches active rules.
|
||||
body := `{"message_content": "This is an urgent matter", "sender_type": "contact", "conversation_status": "open"}`
|
||||
w := s.doRequest(http.MethodPost,
|
||||
fmt.Sprintf("/api/v1/accounts/%d/captain/auto_reply_rules/evaluate", account.ID),
|
||||
@@ -511,8 +508,7 @@ func (s *AutoReplyRuleHandlerTestSuite) TestEvaluate_Success() {
|
||||
|
||||
dataMap, ok := resp.Data.(map[string]interface{})
|
||||
s.Require().True(ok)
|
||||
// ShouldReply will be false because AccountID=0 in evalCtx doesn't match our rule's account_id
|
||||
assert.Equal(s.T(), false, dataMap["should_reply"])
|
||||
assert.Equal(s.T(), true, dataMap["should_reply"])
|
||||
}
|
||||
|
||||
func (s *AutoReplyRuleHandlerTestSuite) TestEvaluate_InvalidJSON() {
|
||||
@@ -555,4 +551,4 @@ func (s *AutoReplyRuleHandlerTestSuite) TestEvaluate_NoActiveRules() {
|
||||
|
||||
func TestAutoReplyRuleHandlerTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(AutoReplyRuleHandlerTestSuite))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,3 +175,29 @@ func TestCaptainPreferencesInvalidAccountID(t *testing.T) {
|
||||
w := f.request(http.MethodGet, "/api/v1/accounts/abc/captain/preferences", nil)
|
||||
require.Equal(t, http.StatusBadRequest, w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
func TestCaptainPreferencesUpdateAndClearBehavior(t *testing.T) {
|
||||
f := newCaptainPreferenceFixture(t)
|
||||
|
||||
w := f.request(http.MethodPut, f.path(""), map[string]any{
|
||||
"behavior": map[string]any{
|
||||
"tone": "friendly",
|
||||
"language": "auto",
|
||||
"max_response_length": 750,
|
||||
"custom_prompt_suffix": "Use short steps",
|
||||
"auto_label_enabled": true,
|
||||
"auto_follow_up_enabled": true,
|
||||
"auto_reply_enabled": false,
|
||||
},
|
||||
})
|
||||
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
||||
behavior := decodeCaptainPreferencePayload(t, w)["behavior"].(map[string]any)
|
||||
require.Equal(t, "Use short steps", behavior["custom_prompt_suffix"])
|
||||
|
||||
w = f.request(http.MethodPut, f.path(""), map[string]any{
|
||||
"behavior": map[string]any{"custom_prompt_suffix": ""},
|
||||
})
|
||||
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
||||
behavior = decodeCaptainPreferencePayload(t, w)["behavior"].(map[string]any)
|
||||
require.Equal(t, "", behavior["custom_prompt_suffix"])
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/llm"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
)
|
||||
|
||||
@@ -16,12 +19,24 @@ import (
|
||||
type CopilotConfigHandler struct {
|
||||
platform *service.CopilotConfigService
|
||||
account *service.CaptainPreferenceService
|
||||
audit *service.AuditService
|
||||
articles *service.ArticleService
|
||||
}
|
||||
|
||||
func NewCopilotConfigHandler(platform *service.CopilotConfigService, account *service.CaptainPreferenceService) *CopilotConfigHandler {
|
||||
return &CopilotConfigHandler{platform: platform, account: account}
|
||||
}
|
||||
|
||||
func (h *CopilotConfigHandler) WithAuditService(audit *service.AuditService) *CopilotConfigHandler {
|
||||
h.audit = audit
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *CopilotConfigHandler) WithArticleService(articles *service.ArticleService) *CopilotConfigHandler {
|
||||
h.articles = articles
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *CopilotConfigHandler) PlatformGet(c *gin.Context) {
|
||||
payload, err := h.platform.Get(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -42,9 +57,62 @@ func (h *CopilotConfigHandler) PlatformUpdate(c *gin.Context) {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
h.recordPlatformUpdate(c, input, payload)
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
func (h *CopilotConfigHandler) recordPlatformUpdate(c *gin.Context, input service.CopilotProviderConfigInput, payload *service.CopilotProviderConfigPayload) {
|
||||
if h.audit == nil || payload == nil {
|
||||
return
|
||||
}
|
||||
changes := map[string]any{
|
||||
"chat": map[string]any{
|
||||
"provider": payload.Chat.Provider,
|
||||
"base_url": payload.Chat.BaseURL,
|
||||
"model": payload.Chat.Model,
|
||||
"api_key_configured": payload.Chat.APIKey.Configured,
|
||||
"api_key_changed": input.Chat.APIKey != "" || input.Chat.ClearAPIKey,
|
||||
},
|
||||
"embedding": map[string]any{
|
||||
"mode": payload.Embedding.Mode,
|
||||
"provider": payload.Embedding.Provider,
|
||||
"base_url": payload.Embedding.BaseURL,
|
||||
"model": payload.Embedding.Model,
|
||||
"dimensions": payload.Embedding.Dimensions,
|
||||
"api_key_configured": payload.Embedding.APIKey.Configured,
|
||||
"api_key_changed": input.Embedding.APIKey != "" || input.Embedding.ClearAPIKey,
|
||||
},
|
||||
"generation": payload.Generation,
|
||||
"request": payload.Request,
|
||||
"configured": payload.Configured,
|
||||
}
|
||||
raw, err := json.Marshal(changes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
audit := &model.Audit{
|
||||
AuditableType: "InstallationConfig",
|
||||
AuditableID: 1,
|
||||
Action: "update",
|
||||
AuditedChanges: raw,
|
||||
RemoteAddress: c.ClientIP(),
|
||||
RequestUUID: firstNonEmpty(c.GetHeader("X-Request-ID"), c.GetHeader("X-Correlation-ID")),
|
||||
Comment: "Copilot provider configuration updated",
|
||||
}
|
||||
if accountID := c.GetUint("account_id"); accountID != 0 {
|
||||
audit.AccountID = &accountID
|
||||
audit.AssociatedType = "Account"
|
||||
audit.AssociatedID = &accountID
|
||||
}
|
||||
if userID := getUserID(c); userID != 0 {
|
||||
audit.UserID = &userID
|
||||
audit.UserType = "SuperAdmin"
|
||||
}
|
||||
if _, err := h.audit.CreateAudit(c.Request.Context(), audit); err != nil {
|
||||
applogger.L().Warnf("Copilot provider audit skipped: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CopilotConfigHandler) PlatformTest(c *gin.Context) {
|
||||
var input service.CopilotProviderConfigInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
@@ -55,7 +123,8 @@ func (h *CopilotConfigHandler) PlatformTest(c *gin.Context) {
|
||||
if err != nil {
|
||||
status := http.StatusUnprocessableEntity
|
||||
if errors.Is(err, llm.ErrProviderNotConfigured) {
|
||||
status = http.StatusConflict
|
||||
response.AbortWithStatusError(c, http.StatusConflict, response.ErrCopilotNotConfigured, err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -63,6 +132,31 @@ func (h *CopilotConfigHandler) PlatformTest(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
func (h *CopilotConfigHandler) PlatformEmbeddingReindexStatus(c *gin.Context) {
|
||||
if h.articles == nil {
|
||||
response.AbortWithStatusError(c, http.StatusServiceUnavailable, response.ErrServiceUnavail, "embedding reindex is not configured")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, h.articles.EmbeddingReindexStatus())
|
||||
}
|
||||
|
||||
func (h *CopilotConfigHandler) PlatformEmbeddingReindexStart(c *gin.Context) {
|
||||
if h.articles == nil {
|
||||
response.AbortWithStatusError(c, http.StatusServiceUnavailable, response.ErrServiceUnavail, "embedding reindex is not configured")
|
||||
return
|
||||
}
|
||||
status, err := h.articles.StartEmbeddingReindex()
|
||||
if err != nil {
|
||||
code := http.StatusUnprocessableEntity
|
||||
if status.Running {
|
||||
code = http.StatusConflict
|
||||
}
|
||||
c.JSON(code, gin.H{"error": err.Error(), "status": status})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusAccepted, status)
|
||||
}
|
||||
|
||||
func (h *CopilotConfigHandler) AccountGet(c *gin.Context) {
|
||||
if !captainPreferencesCanUpdate(c) {
|
||||
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, "administrator role required")
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/llm"
|
||||
"github.com/gochat/gochat/internal/middleware"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type copilotConfigHandlerFixture struct {
|
||||
db *gorm.DB
|
||||
router *gin.Engine
|
||||
account *model.Account
|
||||
}
|
||||
|
||||
func newCopilotConfigHandlerFixture(t *testing.T) *copilotConfigHandlerFixture {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.CaptainPreference{}, &model.InstallationConfig{}, &model.Audit{}))
|
||||
account := &model.Account{Name: "Copilot Config", Active: true}
|
||||
require.NoError(t, db.Create(account).Error)
|
||||
|
||||
manager := llm.NewProviderManager()
|
||||
platformService := service.NewCopilotConfigService(repository.NewInstallationConfigRepo(db), manager)
|
||||
preferenceService := service.NewCaptainPreferenceService(repository.NewCaptainPreferenceRepo(db), repository.NewAccountRepo(db))
|
||||
preferenceService.SetCopilotConfigService(platformService)
|
||||
handler := NewCopilotConfigHandler(platformService, preferenceService).
|
||||
WithAuditService(service.NewAuditService(repository.NewAuditRepo(db)))
|
||||
|
||||
router := gin.New()
|
||||
superAdmin := router.Group("/platform/api/v1/copilot", func(c *gin.Context) {
|
||||
c.Set("user_type", "super_admin")
|
||||
c.Set("account_id", account.ID)
|
||||
c.Next()
|
||||
}, middleware.SuperAdmin())
|
||||
superAdmin.GET("/config", handler.PlatformGet)
|
||||
superAdmin.PUT("/config", handler.PlatformUpdate)
|
||||
superAdmin.POST("/config/test", handler.PlatformTest)
|
||||
|
||||
accountAdmin := router.Group("/api/v1/accounts/:account_id/copilot/config", func(c *gin.Context) {
|
||||
c.Set("role", "administrator")
|
||||
c.Next()
|
||||
})
|
||||
accountAdmin.GET("", handler.AccountGet)
|
||||
accountAdmin.PUT("", handler.AccountUpdate)
|
||||
|
||||
router.PUT("/forbidden/platform/api/v1/copilot/config", func(c *gin.Context) {
|
||||
c.Set("user_type", "user")
|
||||
c.Next()
|
||||
}, middleware.SuperAdmin(), handler.PlatformUpdate)
|
||||
router.GET("/forbidden/api/v1/accounts/:account_id/copilot/config", func(c *gin.Context) {
|
||||
c.Set("role", "agent")
|
||||
c.Next()
|
||||
}, handler.AccountGet)
|
||||
|
||||
t.Cleanup(func() {
|
||||
sqlDB, dbErr := db.DB()
|
||||
require.NoError(t, dbErr)
|
||||
require.NoError(t, sqlDB.Close())
|
||||
})
|
||||
return &copilotConfigHandlerFixture{db: db, router: router, account: account}
|
||||
}
|
||||
|
||||
func (f *copilotConfigHandlerFixture) request(method, path string, body any) *httptest.ResponseRecorder {
|
||||
var raw []byte
|
||||
if body != nil {
|
||||
raw, _ = json.Marshal(body)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(method, path, bytes.NewReader(raw))
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
f.router.ServeHTTP(recorder, req)
|
||||
return recorder
|
||||
}
|
||||
|
||||
func copilotConfigRequest(baseURL, apiKey string) map[string]any {
|
||||
return map[string]any{
|
||||
"chat": map[string]any{
|
||||
"provider": "openai_compatible",
|
||||
"base_url": baseURL,
|
||||
"model": "chat-model",
|
||||
"api_key": apiKey,
|
||||
},
|
||||
"embedding": map[string]any{
|
||||
"mode": "reuse_chat_credentials",
|
||||
"provider": "openai_compatible",
|
||||
"base_url": baseURL,
|
||||
"model": "embedding-model",
|
||||
"dimensions": 3,
|
||||
},
|
||||
"generation": map[string]any{"temperature": 0.2, "max_tokens": 512},
|
||||
"request": map[string]any{"timeout_seconds": 10, "max_retries": 0},
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopilotConfigHandlerPlatformPermissionsAndSecretPresentation(t *testing.T) {
|
||||
f := newCopilotConfigHandlerFixture(t)
|
||||
input := copilotConfigRequest("https://llm.example.com/v1", "plain-secret-key")
|
||||
|
||||
forbidden := f.request(http.MethodPut, "/forbidden/platform/api/v1/copilot/config", input)
|
||||
require.Equal(t, http.StatusForbidden, forbidden.Code, forbidden.Body.String())
|
||||
|
||||
updated := f.request(http.MethodPut, "/platform/api/v1/copilot/config", input)
|
||||
require.Equal(t, http.StatusOK, updated.Code, updated.Body.String())
|
||||
require.NotContains(t, updated.Body.String(), "plain-secret-key")
|
||||
require.Contains(t, updated.Body.String(), "pla****-key")
|
||||
|
||||
var stored model.InstallationConfig
|
||||
require.NoError(t, f.db.Where("name = ?", "COPILOT_CHAT_API_KEY").First(&stored).Error)
|
||||
require.Equal(t, "plain-secret-key", stored.Value)
|
||||
var audit model.Audit
|
||||
require.NoError(t, f.db.Where("auditable_type = ?", "InstallationConfig").First(&audit).Error)
|
||||
require.NotContains(t, string(audit.AuditedChanges), "plain-secret-key")
|
||||
require.NotContains(t, string(audit.AuditedChanges), "pla****-key")
|
||||
require.Contains(t, string(audit.AuditedChanges), `"api_key_changed":true`)
|
||||
require.NotNil(t, audit.AccountID)
|
||||
require.Equal(t, f.account.ID, *audit.AccountID)
|
||||
require.Equal(t, "Account", audit.AssociatedType)
|
||||
|
||||
accountPath := "/api/v1/accounts/" + strconv.FormatUint(uint64(f.account.ID), 10) + "/copilot/config"
|
||||
accountPayload := f.request(http.MethodGet, accountPath, nil)
|
||||
require.Equal(t, http.StatusOK, accountPayload.Code, accountPayload.Body.String())
|
||||
require.NotContains(t, accountPayload.Body.String(), "plain-secret-key")
|
||||
require.NotContains(t, accountPayload.Body.String(), "pla****-key")
|
||||
require.Contains(t, accountPayload.Body.String(), `"configured":true`)
|
||||
|
||||
agentPayload := f.request(http.MethodGet, "/forbidden"+accountPath, nil)
|
||||
require.Equal(t, http.StatusForbidden, agentPayload.Code, agentPayload.Body.String())
|
||||
}
|
||||
|
||||
func TestCopilotConfigHandlerTestEndpointDoesNotPersistCandidate(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/chat/completions":
|
||||
_, _ = w.Write([]byte(`{"id":"chat-1","choices":[{"message":{"role":"assistant","content":"OK"},"finish_reason":"stop"}]}`))
|
||||
case "/embeddings":
|
||||
_, _ = w.Write([]byte(`{"data":[{"index":0,"embedding":[0.1,0.2,0.3]}],"model":"embedding-model"}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
f := newCopilotConfigHandlerFixture(t)
|
||||
tested := f.request(http.MethodPost, "/platform/api/v1/copilot/config/test", copilotConfigRequest(server.URL, "candidate-key"))
|
||||
require.Equal(t, http.StatusOK, tested.Code, tested.Body.String())
|
||||
require.Contains(t, tested.Body.String(), `"ok":true`)
|
||||
require.NotContains(t, tested.Body.String(), "candidate-key")
|
||||
|
||||
current := f.request(http.MethodGet, "/platform/api/v1/copilot/config", nil)
|
||||
require.Equal(t, http.StatusOK, current.Code, current.Body.String())
|
||||
require.Contains(t, current.Body.String(), `"configured":false`)
|
||||
}
|
||||
|
||||
func TestCopilotConfigHandlerUsesStandardNotConfiguredError(t *testing.T) {
|
||||
f := newCopilotConfigHandlerFixture(t)
|
||||
response := f.request(http.MethodPost, "/platform/api/v1/copilot/config/test", map[string]any{})
|
||||
require.Equal(t, http.StatusConflict, response.Code, response.Body.String())
|
||||
require.Contains(t, response.Body.String(), "COPILOT_NOT_CONFIGURED")
|
||||
}
|
||||
|
||||
func TestCopilotProviderErrorsAreSanitized(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.GET("/provider-error", func(c *gin.Context) {
|
||||
handleServiceError(c, fmt.Errorf("provider failed: %w", &llm.APIError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Message: "invalid key sk-secret-value",
|
||||
}))
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/provider-error", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
require.Equal(t, http.StatusBadGateway, recorder.Code)
|
||||
require.Contains(t, recorder.Body.String(), "COPILOT_PROVIDER_AUTHENTICATION_FAILED")
|
||||
require.NotContains(t, recorder.Body.String(), "sk-secret-value")
|
||||
}
|
||||
@@ -88,7 +88,7 @@ func (h *SSEStreamHandler) StreamCopilotMessage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if h.llmProvider == nil {
|
||||
writeSSEMessage(c, "error", `{"error": "Captain is disabled", "status": 422, "done": true}`)
|
||||
writeSSEMessage(c, "error", `{"code": "COPILOT_NOT_CONFIGURED", "error": "Copilot provider is not configured", "status": 503, "done": true}`)
|
||||
writeSSEMessage(c, "done", `{"done": true}`)
|
||||
return
|
||||
}
|
||||
@@ -98,14 +98,15 @@ func (h *SSEStreamHandler) StreamCopilotMessage(c *gin.Context) {
|
||||
|
||||
// Stream from LLM using callback pattern
|
||||
streamReq := llm.ChatRequest{
|
||||
Model: "gpt-4",
|
||||
Model: "",
|
||||
Messages: chatMessages,
|
||||
Temperature: 0.7,
|
||||
MaxTokens: 1024,
|
||||
Stream: true,
|
||||
}
|
||||
|
||||
err = h.llmProvider.ChatCompletionStream(c.Request.Context(), streamReq, func(chunk llm.StreamChunk) error {
|
||||
streamCtx := llm.WithAccountFeature(c.Request.Context(), uint(accountID), "copilot")
|
||||
err = h.llmProvider.ChatCompletionStream(streamCtx, streamReq, func(chunk llm.StreamChunk) error {
|
||||
if len(chunk.Choices) > 0 {
|
||||
content := chunk.Choices[0].Delta.Content
|
||||
if content != "" {
|
||||
|
||||
Reference in New Issue
Block a user