mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2026-11-09 17:06:40 +00:00
Исправления в ветке main
This commit is contained in:
364
gpt/providers.go
364
gpt/providers.go
@@ -3,9 +3,12 @@ package gpt
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -129,9 +132,11 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
||||
fmt.Println("ProxyChatRequest: ", req)
|
||||
}
|
||||
|
||||
config.DebugLogf("[REQUEST] POST %s body=%s", p.BaseURL+config.AppConfig.Server.ProxyUrl, jsonData)
|
||||
resp, err := p.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ошибка выполнения запроса: %w", err)
|
||||
config.DebugLogf("[ERROR] POST %s: %v", p.BaseURL+config.AppConfig.Server.ProxyUrl, err)
|
||||
return "", fmt.Errorf("ошибка выполнения запроса: %w", retryable(err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -140,8 +145,13 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
||||
return "", fmt.Errorf("ошибка чтения ответа: %w", err)
|
||||
}
|
||||
|
||||
config.DebugLogf("[RESPONSE] status=%d body=%s", resp.StatusCode, body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("ошибка API: %d - %s", resp.StatusCode, string(body))
|
||||
err := fmt.Errorf("ошибка API: %d - %s", resp.StatusCode, string(body))
|
||||
if isRetryableStatus(resp.StatusCode) {
|
||||
err = retryable(err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
var response ProxyChatResponse
|
||||
@@ -162,7 +172,9 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
||||
|
||||
// Health для ProxyAPIProvider
|
||||
func (p *ProxyAPIProvider) Health() error {
|
||||
req, err := http.NewRequest("GET", p.BaseURL+config.AppConfig.Server.HealthUrl, nil)
|
||||
url := p.BaseURL + config.AppConfig.Server.HealthUrl
|
||||
config.DebugLogf("[REQUEST] GET %s", url)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка создания health check запроса: %w", err)
|
||||
}
|
||||
@@ -173,20 +185,21 @@ func (p *ProxyAPIProvider) Health() error {
|
||||
|
||||
resp, err := p.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка health check: %w", err)
|
||||
config.DebugLogf("[ERROR] GET %s: %v", url, err)
|
||||
return fmt.Errorf("health check: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка чтения health check ответа: %w", err)
|
||||
}
|
||||
config.DebugLogf("[RESPONSE] status=%d body=%s", resp.StatusCode, body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("health check failed: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var healthResponse ProxyHealthResponse
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка чтения health check ответа: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &healthResponse); err != nil {
|
||||
return fmt.Errorf("ошибка парсинга health check ответа: %w", err)
|
||||
}
|
||||
@@ -233,9 +246,12 @@ func (o *OllamaProvider) Chat(messages []Chat) (string, error) {
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
url := o.BaseURL + "/api/chat"
|
||||
config.DebugLogf("[REQUEST] POST %s body=%s", url, jsonData)
|
||||
resp, err := o.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ошибка выполнения запроса: %w", err)
|
||||
config.DebugLogf("[ERROR] POST %s: %v", url, err)
|
||||
return "", fmt.Errorf("ошибка выполнения запроса: %w", retryable(err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -244,8 +260,13 @@ func (o *OllamaProvider) Chat(messages []Chat) (string, error) {
|
||||
return "", fmt.Errorf("ошибка чтения ответа: %w", err)
|
||||
}
|
||||
|
||||
config.DebugLogf("[RESPONSE] status=%d body=%s", resp.StatusCode, body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("ошибка API: %d - %s", resp.StatusCode, string(body))
|
||||
err := fmt.Errorf("ошибка API: %d - %s", resp.StatusCode, string(body))
|
||||
if isRetryableStatus(resp.StatusCode) {
|
||||
err = retryable(err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
var response OllamaResponse
|
||||
@@ -258,17 +279,26 @@ func (o *OllamaProvider) Chat(messages []Chat) (string, error) {
|
||||
|
||||
// Health для OllamaProvider
|
||||
func (o *OllamaProvider) Health() error {
|
||||
req, err := http.NewRequest("GET", o.BaseURL+"/api/tags", nil)
|
||||
url := o.BaseURL + "/api/tags"
|
||||
config.DebugLogf("[REQUEST] GET %s", url)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка создания health check запроса: %w", err)
|
||||
}
|
||||
|
||||
resp, err := o.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
config.DebugLogf("[ERROR] GET %s: %v", url, err)
|
||||
return fmt.Errorf("ошибка health check: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка чтения health check ответа: %w", err)
|
||||
}
|
||||
|
||||
config.DebugLogf("[RESPONSE] status=%d body=%s", resp.StatusCode, body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("health check failed: %d", resp.StatusCode)
|
||||
}
|
||||
@@ -283,13 +313,16 @@ func (p *ProxyAPIProvider) GetAvailableModels() ([]string, error) {
|
||||
|
||||
// GetAvailableModels возвращает список доступных моделей для провайдера
|
||||
func (o *OllamaProvider) GetAvailableModels() ([]string, error) {
|
||||
req, err := http.NewRequest("GET", o.BaseURL+"/api/tags", nil)
|
||||
url := o.BaseURL + "/api/tags"
|
||||
config.DebugLogf("[REQUEST] GET %s", url)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка создания запроса: %w", err)
|
||||
}
|
||||
|
||||
resp, err := o.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
config.DebugLogf("[ERROR] GET %s: %v", url, err)
|
||||
return nil, fmt.Errorf("ошибка получения моделей: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -299,6 +332,7 @@ func (o *OllamaProvider) GetAvailableModels() ([]string, error) {
|
||||
return nil, fmt.Errorf("ошибка чтения ответа: %w", err)
|
||||
}
|
||||
|
||||
config.DebugLogf("[RESPONSE] status=%d body=%s", resp.StatusCode, body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ошибка API: %d - %s", resp.StatusCode, string(body))
|
||||
}
|
||||
@@ -315,3 +349,307 @@ func (o *OllamaProvider) GetAvailableModels() ([]string, error) {
|
||||
|
||||
return models, nil
|
||||
}
|
||||
|
||||
// ApimiraProvider реализация для OpenAI-совместимого прокси apimira
|
||||
type ApimiraProvider struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
KeyFile string
|
||||
Model string
|
||||
CompletionsPath string
|
||||
Temperature float64
|
||||
HTTPClient *http.Client
|
||||
modelErr error
|
||||
}
|
||||
|
||||
// ApimiraChatRequest структура запроса в формате OpenAI Chat Completions
|
||||
type ApimiraChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Chat `json:"messages"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
}
|
||||
|
||||
// ApimiraChatResponse структура ответа OpenAI Chat Completions
|
||||
type ApimiraChatResponse struct {
|
||||
Choices []struct {
|
||||
Message Chat `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
// ApimiraError структура ошибки в формате OpenAI
|
||||
type ApimiraError struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ApimiraErrorResponse обертка для ошибки в формате OpenAI
|
||||
type ApimiraErrorResponse struct {
|
||||
Error ApimiraError `json:"error"`
|
||||
}
|
||||
|
||||
// ApimiraModelsResponse структура ответа GET /v1/models
|
||||
type ApimiraModelsResponse struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func NewApimiraProvider(baseURL, apiKey, model, completionsPath string, temperature float64, timeout int) *ApimiraProvider {
|
||||
keyFile := ""
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
keyFile = filepath.Join(home, config.AppConfig.ApiKeyFile)
|
||||
}
|
||||
|
||||
model = strings.TrimSpace(model)
|
||||
completionsPath = strings.TrimSpace(completionsPath)
|
||||
if completionsPath == "" {
|
||||
completionsPath = "/v1/chat/completions"
|
||||
}
|
||||
if !strings.HasPrefix(completionsPath, "/") {
|
||||
completionsPath = "/" + completionsPath
|
||||
}
|
||||
|
||||
return &ApimiraProvider{
|
||||
BaseURL: strings.TrimSuffix(baseURL, "/"),
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
KeyFile: keyFile,
|
||||
Model: model,
|
||||
CompletionsPath: completionsPath,
|
||||
Temperature: temperature,
|
||||
HTTPClient: &http.Client{Timeout: time.Duration(timeout) * time.Second},
|
||||
modelErr: validateApimiraModel(model),
|
||||
}
|
||||
}
|
||||
|
||||
// validateApimiraModel проверяет обязательный формат модели поставщик/имя_модели
|
||||
func validateApimiraModel(model string) error {
|
||||
if model == "" {
|
||||
return fmt.Errorf("для провайдера apimira модель обязательна в формате поставщик/имя_модели (например anthropic/claude-opus-4.7): задайте LCG_MODEL")
|
||||
}
|
||||
parts := strings.Split(model, "/")
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" || strings.ContainsAny(model, " \t\n") {
|
||||
return fmt.Errorf("модель apimira %q указана в неверном формате: ожидается поставщик/имя_модели, например anthropic/claude-opus-4.7", model)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveKey возвращает ключ из переменной или файла (права файла проверяются и исправляются на 0600)
|
||||
func (p *ApimiraProvider) resolveKey() (string, error) {
|
||||
if p.APIKey != "" {
|
||||
return p.APIKey, nil
|
||||
}
|
||||
if p.KeyFile == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(p.KeyFile)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if info, err := os.Stat(p.KeyFile); err == nil && info.Mode().Perm()&0o077 != 0 {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Файл с ключом apimira %s имеет права доступа %v, устанавливаю 0600\n", p.KeyFile, info.Mode().Perm())
|
||||
if err := os.Chmod(p.KeyFile, 0o600); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Не удалось изменить права доступа %s: %v\n", p.KeyFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(string(data))
|
||||
p.APIKey = key
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// requireKey возвращает ключ или понятную ошибку о том, как его задать
|
||||
func (p *ApimiraProvider) requireKey() (string, error) {
|
||||
key, err := p.resolveKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if key == "" {
|
||||
return "", fmt.Errorf("не задан API ключ apimira: установите LCG_JWT_TOKEN или выполните команду update-key (ключ также читается из файла %s, права 0600)", p.KeyFile)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func apimiraHTTPError(status int, body []byte) error {
|
||||
var errResp ApimiraErrorResponse
|
||||
if err := json.Unmarshal(body, &errResp); err == nil && errResp.Error.Message != "" {
|
||||
return fmt.Errorf("ошибка apimira: %d %s", status, errResp.Error.Message)
|
||||
}
|
||||
return fmt.Errorf("ошибка apimira: %d - %s", status, string(body))
|
||||
}
|
||||
|
||||
// retryableError маркирует временные ошибки (сеть, 429, 5xx), при которых имеет смысл повторять запрос
|
||||
type retryableError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *retryableError) Error() string { return e.err.Error() }
|
||||
func (e *retryableError) Unwrap() error { return e.err }
|
||||
|
||||
func retryable(err error) error {
|
||||
return &retryableError{err: err}
|
||||
}
|
||||
|
||||
func isRetryableStatus(status int) bool {
|
||||
return status == http.StatusTooManyRequests || status >= http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// IsRetryable сообщает, можно ли повторять запрос после этой ошибки
|
||||
func IsRetryable(err error) bool {
|
||||
var re *retryableError
|
||||
return errors.As(err, &re)
|
||||
}
|
||||
|
||||
// Chat для ApimiraProvider (POST, формат OpenAI Chat Completions, без стриминга)
|
||||
func (p *ApimiraProvider) Chat(messages []Chat) (string, error) {
|
||||
if p.modelErr != nil {
|
||||
return "", p.modelErr
|
||||
}
|
||||
key, err := p.requireKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
payload := ApimiraChatRequest{
|
||||
Model: p.Model,
|
||||
Messages: messages,
|
||||
Temperature: p.Temperature,
|
||||
Stream: false,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ошибка маршалинга запроса: %w", err)
|
||||
}
|
||||
|
||||
url := p.BaseURL + p.CompletionsPath
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ошибка создания запроса: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+key)
|
||||
|
||||
if config.AppConfig.MainFlags.Debug {
|
||||
fmt.Println("Chat URL: ", url)
|
||||
}
|
||||
|
||||
config.DebugLogf("[REQUEST] POST %s body=%s", url, jsonData)
|
||||
resp, err := p.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
config.DebugLogf("[ERROR] POST %s: %v", url, err)
|
||||
return "", fmt.Errorf("ошибка выполнения запроса: %w", retryable(err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ошибка чтения ответа: %w", err)
|
||||
}
|
||||
|
||||
config.DebugLogf("[RESPONSE] status=%d body=%s", resp.StatusCode, body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err := apimiraHTTPError(resp.StatusCode, body)
|
||||
if isRetryableStatus(resp.StatusCode) {
|
||||
err = retryable(err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
var response ApimiraChatResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return "", fmt.Errorf("ошибка парсинга ответа apimira: %w", err)
|
||||
}
|
||||
|
||||
if len(response.Choices) == 0 {
|
||||
return "", fmt.Errorf("пустой ответ от apimira: нет choices в ответе")
|
||||
}
|
||||
if strings.TrimSpace(response.Choices[0].Message.Content) == "" {
|
||||
return "", fmt.Errorf("пустой ответ от apimira (finish_reason: %s)", response.Choices[0].FinishReason)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(response.Choices[0].Message.Content), nil
|
||||
}
|
||||
|
||||
// Health для ApimiraProvider — проверка доступности через GET /v1/models
|
||||
func (p *ApimiraProvider) Health() error {
|
||||
if p.modelErr != nil {
|
||||
return p.modelErr
|
||||
}
|
||||
key, err := p.requireKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", p.BaseURL+"/v1/models", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка создания health check запроса: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+key)
|
||||
|
||||
config.DebugLogf("[REQUEST] GET %s", p.BaseURL+"/v1/models")
|
||||
resp, err := p.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
config.DebugLogf("[ERROR] GET %s: %v", p.BaseURL+"/v1/models", err)
|
||||
return fmt.Errorf("health check: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка чтения health check ответа: %w", err)
|
||||
}
|
||||
config.DebugLogf("[RESPONSE] status=%d body=%s", resp.StatusCode, body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return apimiraHTTPError(resp.StatusCode, body)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAvailableModels для ApimiraProvider — GET /v1/models в формате OpenAI
|
||||
func (p *ApimiraProvider) GetAvailableModels() ([]string, error) {
|
||||
key, err := p.requireKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", p.BaseURL+"/v1/models", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка создания запроса: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+key)
|
||||
|
||||
config.DebugLogf("[REQUEST] GET %s", p.BaseURL+"/v1/models")
|
||||
resp, err := p.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
config.DebugLogf("[ERROR] GET %s: %v", p.BaseURL+"/v1/models", err)
|
||||
return nil, fmt.Errorf("ошибка получения моделей: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка чтения ответа: %w", err)
|
||||
}
|
||||
config.DebugLogf("[RESPONSE] status=%d body=%s", resp.StatusCode, body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, apimiraHTTPError(resp.StatusCode, body)
|
||||
}
|
||||
|
||||
var response ApimiraModelsResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, fmt.Errorf("ошибка парсинга ответа: %w", err)
|
||||
}
|
||||
|
||||
var models []string
|
||||
for _, model := range response.Data {
|
||||
models = append(models, model.ID)
|
||||
}
|
||||
|
||||
return models, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user