| package service |
|
|
| import ( |
| "bufio" |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "io" |
| "net/http" |
| "time" |
|
|
| "github.com/google/uuid" |
| "zencoder-2api/internal/model" |
| ) |
|
|
| const ( |
| ZencoderChatURL = "https://api.zencoder.ai/v1/chat/completions" |
| MaxRetries = 3 |
| ZencoderVersion = "3.24.0" |
| ) |
|
|
| type ZencoderService struct{} |
|
|
| func NewZencoderService() *ZencoderService { |
| return &ZencoderService{} |
| } |
|
|
| func setZencoderHeaders(req *http.Request, token, modelID string) { |
| req.Header.Set("Accept", "application/json") |
| req.Header.Set("Content-Type", "application/json") |
| req.Header.Set("Authorization", "Bearer "+token) |
| req.Header.Set("User-Agent", "zen-cli/0.9.0-windows-x64") |
| req.Header.Set("zen-model-id", modelID) |
| req.Header.Set("zencoder-arch", "x64") |
| req.Header.Set("zencoder-os", "windows") |
| req.Header.Set("zencoder-version", ZencoderVersion) |
| req.Header.Set("zencoder-client-type", "vscode") |
| req.Header.Set("zencoder-operation-id", uuid.New().String()) |
| req.Header.Set("zencoder-operation-type", "agent_call") |
| } |
|
|
| func (s *ZencoderService) Chat(req *model.ChatCompletionRequest) (*model.ChatCompletionResponse, error) { |
| |
| zenModel, exists := model.GetZenModel(req.Model) |
| if !exists { |
| return nil, ErrNoAvailableAccount |
| } |
|
|
| var lastErr error |
| for i := 0; i < MaxRetries; i++ { |
| account, err := GetNextAccountForModel(req.Model) |
| if err != nil { |
| return nil, err |
| } |
|
|
| resp, err := s.doRequest(account, req) |
| if err != nil { |
| MarkAccountError(account) |
| lastErr = err |
| continue |
| } |
|
|
| ResetAccountError(account) |
| |
| |
| UseCredit(account, zenModel.Multiplier) |
| |
| return resp, nil |
| } |
|
|
| return nil, fmt.Errorf("all retries failed: %w", lastErr) |
| } |
|
|
| func (s *ZencoderService) doRequest(account *model.Account, req *model.ChatCompletionRequest) (*model.ChatCompletionResponse, error) { |
| token, err := GetToken(account) |
| if err != nil { |
| return nil, err |
| } |
|
|
| |
| zenModel, exists := model.GetZenModel(req.Model) |
| if !exists { |
| return nil, ErrNoAvailableAccount |
| } |
|
|
| body, err := json.Marshal(req) |
| if err != nil { |
| return nil, err |
| } |
|
|
| client := createHTTPClient(account.Proxy) |
| httpReq, err := http.NewRequest("POST", ZencoderChatURL, bytes.NewReader(body)) |
| if err != nil { |
| return nil, err |
| } |
|
|
| setZencoderHeaders(httpReq, token, zenModel.ID) |
|
|
| resp, err := client.Do(httpReq) |
| if err != nil { |
| return nil, err |
| } |
| defer resp.Body.Close() |
|
|
| respBody, err := io.ReadAll(resp.Body) |
| if err != nil { |
| return nil, err |
| } |
|
|
| if resp.StatusCode != http.StatusOK { |
| return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(respBody)) |
| } |
|
|
| var chatResp model.ChatCompletionResponse |
| if err := json.Unmarshal(respBody, &chatResp); err != nil { |
| return nil, err |
| } |
|
|
| return &chatResp, nil |
| } |
|
|
| func (s *ZencoderService) ChatStream(req *model.ChatCompletionRequest, writer http.ResponseWriter) error { |
| |
| zenModel, exists := model.GetZenModel(req.Model) |
| if !exists { |
| return ErrNoAvailableAccount |
| } |
|
|
| var lastErr error |
| for i := 0; i < MaxRetries; i++ { |
| account, err := GetNextAccountForModel(req.Model) |
| if err != nil { |
| return err |
| } |
|
|
| err = s.doStreamRequest(account, req, writer) |
| if err != nil { |
| MarkAccountError(account) |
| lastErr = err |
| continue |
| } |
|
|
| ResetAccountError(account) |
| |
| |
| UseCredit(account, zenModel.Multiplier) |
| |
| return nil |
| } |
|
|
| return fmt.Errorf("all retries failed: %w", lastErr) |
| } |
|
|
| func (s *ZencoderService) doStreamRequest(account *model.Account, req *model.ChatCompletionRequest, writer http.ResponseWriter) error { |
| token, err := GetToken(account) |
| if err != nil { |
| return err |
| } |
|
|
| |
| zenModel, exists := model.GetZenModel(req.Model) |
| if !exists { |
| return ErrNoAvailableAccount |
| } |
|
|
| req.Stream = true |
| body, err := json.Marshal(req) |
| if err != nil { |
| return err |
| } |
|
|
| client := createHTTPClient(account.Proxy) |
| client.Timeout = 5 * time.Minute |
|
|
| httpReq, err := http.NewRequest("POST", ZencoderChatURL, bytes.NewReader(body)) |
| if err != nil { |
| return err |
| } |
|
|
| setZencoderHeaders(httpReq, token, zenModel.ID) |
|
|
| resp, err := client.Do(httpReq) |
| if err != nil { |
| return err |
| } |
| defer resp.Body.Close() |
|
|
| if resp.StatusCode != http.StatusOK { |
| respBody, _ := io.ReadAll(resp.Body) |
| return fmt.Errorf("status %d: %s", resp.StatusCode, string(respBody)) |
| } |
|
|
| return s.streamResponse(resp.Body, writer) |
| } |
|
|
| func (s *ZencoderService) streamResponse(body io.Reader, writer http.ResponseWriter) error { |
| flusher, ok := writer.(http.Flusher) |
| if !ok { |
| return fmt.Errorf("streaming not supported") |
| } |
|
|
| writer.Header().Set("Content-Type", "text/event-stream") |
| writer.Header().Set("Cache-Control", "no-cache") |
| writer.Header().Set("Connection", "keep-alive") |
|
|
| scanner := bufio.NewScanner(body) |
| for scanner.Scan() { |
| line := scanner.Text() |
| if line == "" { |
| continue |
| } |
| fmt.Fprintf(writer, "%s\n\n", line) |
| flusher.Flush() |
| } |
|
|
| return scanner.Err() |
| } |
|
|