Files
jellybit/internal/llm/llm.go
T
av fc9a3b4066 распознавание: большие раздачи размечаются целиком, файлы вне плана видны
- модель адресует файл номером строки нашего списка вместо копии пути: ответ
  на 180 файлов вместо ~15k токенов стоит ~2.5k, усечение сотней снято,
  max_files и max_tokens ушли в [recognition], correction-ретрай больше не
  переприсылает список
- негодный элемент ответа отбрасывается поимённой причиной, обрыв генерации и
  отказ по размеру запроса названы своими причинами, покрытие плана блокирует
  авто только при непокрытом видеофайле
- раскладка показывает все файлы раздачи со строками «не в плане» и полным
  порядком сортировки; снимок списка файлов лёг рядом с планом (миграция 0012)
2026-09-02 08:54:58 +03:00

112 lines
4.7 KiB
Go

// Package llm — провайдер LLM за интерфейсом (дискриминатор type).
//
// Реализация выбирается полем [llm].type (см. openspec/specs/recognition/spec.md).
// Первый и пока единственный тип — "openai-compat": OpenAI-совместимый Chat
// Completions API (локальные серверы LM Studio/llama.cpp/Ollama и облачные
// совместимые провайдеры — DeepSeek, Qwen и др.).
//
// Пакет отвечает за один вызов модели и транспортную устойчивость (ретраи
// на сетевые сбои, 429 и 5xx). Бюджет переразбора ответа со схемой-в-промпте
// (llm.max_retries) принадлежит вызывающему recognize, а не провайдеру.
package llm
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
)
// Role — роль сообщения в диалоге.
type Role string
const (
RoleSystem Role = "system"
RoleUser Role = "user"
RoleAssistant Role = "assistant"
)
// Message — одно сообщение запроса.
type Message struct {
Role Role
Content string
}
// Request — запрос к модели.
type Request struct {
Messages []Message
JSONMode bool // response_format: json_object (структурированный вывод)
Temperature *float64 // nil — не передаём, у модели остаётся её дефолт
MaxTokens int // 0 — не передаём
}
// Usage — расход токенов и стоимость (если провайдер их сообщает).
type Usage struct {
PromptTokens int
CompletionTokens int
TotalTokens int
Cost float64
}
// Response — ответ модели.
type Response struct {
Content string
Model string
Usage Usage
// FinishReason — причина завершения генерации, как её назвал провайдер
// ("stop", "length", …). Пусто — провайдер её не сообщил. Нужна
// вызывающему: обрыв по длине неотличим от мусора по одному лишь телу
// ответа, а повторять такой запрос бессмысленно (см. recognize).
FinishReason string
}
// FinishLength — значение FinishReason, которым OpenAI-совместимые провайдеры
// сообщают обрыв генерации по достижении предела токенов.
const FinishLength = "length"
// Provider — абстракция доступа к LLM. recognize работает только с ним и не
// знает про конкретный транспорт.
type Provider interface {
Complete(ctx context.Context, req Request) (Response, error)
}
// Config — параметры провайдера (подмножество [llm] из конфига).
type Config struct {
Type string
BaseURL string
APIKey string
Model string
Proxy string
Timeout time.Duration
}
// ErrUnknownType — запрошенный [llm].type не поддерживается.
var ErrUnknownType = errors.New("llm: unknown provider type")
// ErrRequestTooLarge — провайдер отказал из-за размера самого запроса
// (переполнение контекста), а не из-за его содержимого. Вызывающему это
// отдельная ветвь: такой отказ не лечится повтором, а называется человеку
// своей причиной и крутится настройкой ([recognition].max_files).
//
// Признак ставит транспорт — там, где ещё целы HTTP-статус и полное тело
// ответа (см. openai.go). Вызывающий проверяет его errors.Is: у итоговой
// ошибки текст обрезан и в него попадает эхо запроса, так что матчить по нему
// нельзя (docs/conventions/errors.md).
var ErrRequestTooLarge = errors.New("llm: request too large")
// New собирает провайдер по дискриминатору cfg.Type. logger nil → slog.Default().
func New(cfg Config, logger *slog.Logger) (Provider, error) {
if logger == nil {
logger = slog.Default()
}
switch cfg.Type {
case "openai-compat":
return newOpenAICompat(cfg, logger)
case "":
return nil, fmt.Errorf("%w: %q (set [llm].type)", ErrUnknownType, cfg.Type)
default:
return nil, fmt.Errorf("%w: %q", ErrUnknownType, cfg.Type)
}
}