No description
Find a file
2026-06-01 13:55:15 +03:00
cmd Добавление режима stream 2026-06-01 06:32:38 +03:00
.gitignore Добавление параллелизма, callback-функций и управление NeedContinue 2026-06-01 11:34:00 +03:00
control_test.go Добавление параллелизма, callback-функций и управление NeedContinue 2026-06-01 11:34:00 +03:00
embedding.go Модификация бибилотеки 2026-05-31 16:29:34 +03:00
embedding_test.go Модификация бибилотеки 2026-05-31 16:29:34 +03:00
errors.go Улучшение tools 2026-06-01 13:43:26 +03:00
go.mod Обновление документации 2026-06-01 13:55:15 +03:00
go.sum Модификация бибилотеки 2026-05-31 16:29:34 +03:00
history_test.go Модификация бибилотеки 2026-05-31 16:29:34 +03:00
http.go Модификация бибилотеки 2026-05-31 16:29:34 +03:00
http_test.go Добавление тестов 2026-05-31 15:42:49 +03:00
LICENSE first commit 2025-11-12 11:42:46 +03:00
llm.go Добавление параллелизма, callback-функций и управление NeedContinue 2026-06-01 11:34:00 +03:00
llm_integration_test.go Добавление поддержки рассуждения 2026-06-01 11:47:20 +03:00
llm_test.go Добавление поддержки рассуждения 2026-06-01 11:47:20 +03:00
README.md Обновление документации 2026-06-01 13:55:15 +03:00
stream.go Добавление поддержки рассуждения 2026-06-01 11:47:20 +03:00
stream_test.go Добавление поддержки рассуждения 2026-06-01 11:47:20 +03:00
structs.go Добавление поддержки рассуждения 2026-06-01 11:47:20 +03:00
tools.go Улучшение tools 2026-06-01 13:43:26 +03:00
tools_test.go Улучшение tools 2026-06-01 13:43:26 +03:00
vision.go Добавление тестов 2026-05-31 15:42:49 +03:00
vision_test.go Добавление тестов 2026-05-31 15:42:49 +03:00

llm-simple-api

Go Reference License: MIT

Простая Go библиотека для взаимодействия с LLM (Language Model) API.

Описание

Библиотека предоставляет удобный интерфейс для работы с различными LLM API, включая поддержку Open-WebUI, OpenAI, LM Studio и других совместимых сервисов. Предоставляет функционал для отправки сообщений, управления историей чата и настройки параметров модели.

Возможности

  • Поддержка различных LLM API (Open-WebUI, OpenAI, LM Studio и др.)
  • Управление историей чата (чтение, модификация, очистка)
  • Настройка параметров модели (температура, максимальное количество токенов)
  • Поддержка авторизации через API токены
  • Настройка TLS и таймаутов
  • Таймаут на отдельное сообщение (MsgTimeout)
  • Получение эмбеддингов (совместимо с OpenAI API)
  • Система инструментов (function calling) с автогенерацией JSON Schema
  • Автоматическая обработка вызовов инструментов (round-trip)
  • Отправка vision-запросов (изображения)
  • Сжатие истории чата при превышении бюджета токенов
  • Инъекция кастомного HTTP-клиента
  • Отдельный URL для сервиса эмбеддингов
  • Ручное управление циклом tool calls (NotContinue / Continue)
  • Параллельное выполнение инструментов (MaxParallelTools)
  • Callback'и жизненного цикла (OnBeforeLLM, OnAfterLLM, OnBeforeCallTool, OnAfterCallTool)
  • Reasoning Content (рассуждения моделей: DeepSeek-R1, QwQ и др.)

Установка

go get git.ymnuktech.ru/ymnuk/llm-simple-api

Использование

package main

import (
    "fmt"
    "git.ymnuktech.ru/ymnuk/llm-simple-api/llm"
)

func main() {
    opts := &llm.LLMOpts{
        BaseUrl:   "http://localhost:11434/ollama",
        Model:     "gpt-3.5-turbo",
        Token:     stringPtr("your-api-token"),
        Timeout:   30 * time.Second,
    }

    client := llm.NewLLMSimpleAPI(opts)

    // Отправка сообщения
    message, err := client.ChatCompletions("Привет, как дела?")
    if err != nil {
        panic(err)
    }
    fmt.Println(message.Content)

    // Получение эмбеддингов
    embeddings, err := client.Embedding([]string{"Привет, мир!", "Как дела?"})
    if err != nil {
        panic(err)
    }
    for _, data := range embeddings.Data {
        fmt.Printf("Embedding %d: %v\n", data.Index, data.Embedding)
    }
}

Структуры данных

LLMOpts

Поле Тип Дефолт Описание
BaseUrl string http://localhost:11434/ollama Базовый URL API
TlsVerify bool false Проверка TLS сертификата
Timeout time.Duration 1m Таймаут HTTP-клиента
MsgTimeout time.Duration 0 Таймаут на одно сообщение (0 = отключён)
Token *string nil API токен (Bearer)
UserAgent string llm-simple-api User-Agent заголовок
SystemPrompt string Ты - ассистент... Системный промпт
Model string gpt-3.5-turbo Модель по умолчанию
Temperature float32 0.8 Температура генерации
MaxTokens int 8192 Максимальное количество токенов
ModelEmbed string nomic-embed-text Модель для эмбеддингов
ModelEmbedCustomPath string /embeddings Путь для эмбеддингов
EmbeddingBaseUrl string "" Отдельный URL для эмбеддингов (пусто = BaseUrl)
HTTPClient *http.Client nil Кастомный HTTP-клиент (nil = дефолтный)
History *HistoryOpts nil Опции сжатия истории (nil = отключено)
ImageType string png Формат изображения (png, jpeg/jpg)
MaxWidth int 1024 Макс. ширина изображения
MaxHeight int 1024 Макс. высота изображения
NotContinue bool false Остановить loop после tool calls (ручное управление)
MaxParallelTools int 0 Параллельное выполнение инструментов (>1 = пул горутин)
OnBeforeLLM func(prompt string) nil Callback перед каждым запросом к LLM
OnAfterLLM func(*Message) nil Callback после каждого ответа от LLM
OnBeforeCallTool func(ToolCall) nil Callback перед выполнением инструмента
OnAfterCallTool func(ToolCallResponse) nil Callback после выполнения инструмента

Message

Сообщение в чате:

  • Role - роль (user, assistant, system, tool)
  • Content - содержание сообщения
  • ToolCalls - вызовы инструментов от assistant
  • ToolCallID - ID вызова для tool-ответов
  • Reasoning - цепочка рассуждений (для reasoning-моделей)

EmbeddingResponse

Ответ с эмбеддингами (совместим с OpenAI API):

  • Object - тип объекта ("list")
  • Data - массив объектов с эмбеддингами:
    • Object - тип объекта ("embedding")
    • Embedding - массив float32 значений
    • Index - индекс текста в запросе
  • Model - использованная модель
  • Usage - информация об использовании:
    • PromptTokens - количество токенов во входных данных
    • CompletionTokens - количество токенов в ответе
    • TotalTokens - общее количество токенов

Usage

Информация об использовании токенов:

  • PromptTokens - токены в промпте
  • CompletionTokens - токены в ответе
  • TotalTokens - всего токенов

HistoryOpts

Опции сжатия истории чата:

  • MaxTokens - бюджет токенов (0 = отключено)
  • Threshold - порог срабатывания [0;1], дефолт 0.8
  • OnLimit - callback, вызываемый при превышении порога

Методы

NewLLMSimpleAPI

Создание нового клиента LLM. Если передан opts.HTTPClient, используется он, иначе создаётся дефолтный с настройками из LLMOpts.

ChatCompletions

Отправка запроса в модель и получение ответа. Автоматически управляет историей чата и обрабатывает вызовы инструментов.

Embedding

Получение эмбеддингов для текста или массива текстов. Если задан EmbeddingBaseUrl, используется отдельный URL, иначе BaseUrl.

ClearHistory

Очистка истории чата. Сбрасывает накопленный usage.

GetHistory

Получение текущей истории чата (можно модифицировать).

SetHistory

Замена всей истории чата. Используется агентом для пересборки контекста (например, после сжатия или суммаризации):

h := client.GetHistory()
summary := compressHistory(h) // логика агента
client.SetHistory(append(h[:1], summary))

LastUsage

Возвращает *Usage последнего ответа, или nil если запросов ещё не было.

TotalUsage

Возвращает кумулятивный Usage за всю сессию.

SendVisionRequest

Отправка запроса с изображением в модель, поддерживающую визуальное восприятие. Функция принимает текстовый запрос и изображение в формате image.Image, преобразует изображение в base64 строку с учётом настроек формата и размера.

Управление контекстом (HistoryOpts)

Библиотека позволяет контролировать расход токенов на историю чата. После каждого успешного ответа проверяется соотношение prompt_tokens / MaxTokens. Если оно превышает Threshold — вызывается callback OnLimit, который может сжать историю.

opts := &llm.LLMOpts{
    BaseUrl: "http://192.168.0.200:1234/v1",
    Model:   "google/gemma-4-e2b",
    History: &llm.HistoryOpts{
        MaxTokens: 4096,
        Threshold: 0.8,
        OnLimit: func(h []llm.Message, u llm.Usage) ([]llm.Message, error) {
            // Оставляем system + последние 10 сообщений
            return append(h[:1], h[len(h)-10:]...), nil
        },
    },
}

Callback получает текущую историю и usage последнего ответа. Если вернуть ошибку — история не меняется.

Таймаут на сообщение (MsgTimeout)

Позволяет задать максимальное время выполнения одного запроса, независимо от глобального Timeout HTTP-клиента:

opts := &llm.LLMOpts{
    BaseUrl:    "http://192.168.0.200:1234/v1",
    Model:      "google/gemma-4-e2b",
    MsgTimeout: 30 * time.Second, // таймаут на одно сообщение
}

При превышении возвращается context.DeadlineExceeded. Если MsgTimeout = 0 (по умолчанию) — таймаут не применяется.

Кастомный HTTP-клиент

Можно передать собственный *http.Client для тонкой настройки транспорта, прокси или тестирования:

customClient := &http.Client{
    Timeout: 10 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns: 100,
    },
}

opts := &llm.LLMOpts{
    BaseUrl:    "http://192.168.0.200:1234/v1",
    Model:      "google/gemma-4-e2b",
    HTTPClient: customClient,
}

Функциональность инструментов

Библиотека включает в себя систему инструментов, которая позволяет регистрировать пользовательские функции, вызываемые LLM. Это аналогично function calling в OpenAI.

Возможности

  • Автоматическая генерация JSON-схемы: генерирует JSON-схему из Go-структур через рефлексию
  • Регистрация функций: регистрируйте любую функцию с сигнатурой func(SomeStruct) (interface{}, error)
  • Автоматическая отправка инструментов: зарегистрированные инструменты автоматически отправляются с запросами
  • Автоматическая обработка вызовов: библиотека выполняет инструменты и возвращает результат в LLM
  • Полный round-trip: при наличии tool calls библиотека делает второй запрос с результатами
  • Типизированные ошибки: ErrToolAlreadyRegistered, ErrToolNotFound, ErrInvalidToolSignature — совместимы с errors.Is
  • JSON Schema improvements: поля с omitempty и поля-указатели не попадают в required; minimum/maximum парсятся из struct-тегов

Использование

  1. Определите структуру параметров с JSON-тегами:

    type WeatherParams struct {
        Location string `json:"location" description:"Местоположение для получения погоды"`
    }
    
  2. Определите функцию:

    func GetWeather(params WeatherParams) (interface{}, error) {
        return map[string]interface{}{
            "temperature": 22.5,
            "location":    params.Location,
            "condition":   "Солнечно",
        }, nil
    }
    
  3. Зарегистрируйте инструмент:

    api := llm.NewLLMSimpleAPI(nil)
    api.RegisterTool("get_weather", "Получить информацию о погоде", GetWeather)
    
  4. Используйте обычный ChatCompletions:

    message, err := api.ChatCompletions("Какая погода в Москве?")
    

    Если LLM решит вызвать инструмент, библиотека автоматически выполнит вызов и отправит результат обратно, вернув финальный ответ.

Типизированные ошибки

Система инструментов возвращает типизированные ошибки, совместимые с errors.Is и errors.As:

Тип Когда возникает
ErrToolAlreadyRegistered{Name} Регистрация инструмента с уже существующим именем
ErrToolNotFound{Name} Выполнение незарегистрированного инструмента
ErrInvalidToolSignature{Msg} Невалидная сигнатура функции (не функция, не те параметры, не те возвраты)
err := api.RegisterTool("get_weather", "desc", GetWeather)
if errors.As(err, &llm.ErrToolAlreadyRegistered{}) {
    fmt.Println("инструмент уже существует")
}

err = api.RegisterTool("invalid", "desc", "not a function")
var sigErr llm.ErrInvalidToolSignature
if errors.As(err, &sigErr) {
    fmt.Println(sigErr.Msg) // "handler must be a function"
}

Улучшения JSON Schema

  • omitempty: поля с json:"...,omitempty" не попадают в required — модель не обязана их указывать
  • Указатели: поля-указатели (*string, *float64) не попадают в required, даже без omitempty
  • minimum/maximum: числовые поля могут использовать struct-теги для ограничения диапазона:
type WeatherParams struct {
    Location string  `json:"location"`
    Temp     float64 `json:"temp" minimum:"-50" maximum:"60"`
    Count    int     `json:"count" minimum:"1" maximum:"100"`
}

Генерируемая JSON Schema:

{
  "temp":  {"type": "number", "minimum": -50, "maximum": 60},
  "count": {"type": "integer", "minimum": 1, "maximum": 100}
}

Стриминг (ChatCompletionsStream)

ChatCompletionsStream — стриминговая версия ChatCompletions. Ответ приходит по частям через callback. Оба раунда (первичный ответ и повторный после tool calls) стримятся.

StreamChunk

Поле Тип Описание
Content string Дельта текста с момента прошлого chunk
Reasoning string Дельта reasoning_content (рассуждение модели)
Finish string Причина завершения раунда: "stop", "tool_calls", "length", ""
ToolCalls []ToolCall Заполнено только при Finish: "tool_calls"
Err error Ошибка парсинга SSE
Done bool true когда ВЕСЬ стриминг завершён (все раунды)

Использование

response, err := api.ChatCompletionsStream("Какая погода в Москве?", func(chunk llm.StreamChunk) {
    if chunk.Err != nil {
        fmt.Printf("Error: %v\n", chunk.Err)
        return
    }
    if len(chunk.ToolCalls) > 0 {
        fmt.Printf("\n[выполняю %d инструментов]\n", len(chunk.ToolCalls))
        return
    }
    fmt.Print(chunk.Content)  // печатаем текст по мере поступления
})
if err != nil {
    log.Fatal(err)
}

Callback безопасно вызывается с nil — если callback не нужен, передайте nil.

Как работает стриминг tool calls

  1. LLM начинает генерировать текст — callback получает Content с каждым chunk
  2. LLM решает вызвать инструмент — callback получает Finish: "tool_calls" и ToolCalls
  3. Библиотека выполняет инструменты (синхронно)
  4. Начинается второй стриминговый раунд — callback снова получает Content
  5. Второй раунд завершается — callback получает Done: true

IsNeedContinue / Continue / ContinueStream

NotContinue в LLMOpts переключает библиотеку в режим ручного управления циклом tool calls. По умолчанию (false) библиотека автоматически выполняет инструменты и делает повторный запрос к LLM. С NotContinue: true после tool calls library возвращает управление агенту.

opts := &llm.LLMOpts{
    BaseUrl:     "http://192.168.0.200:1234/v1",
    Model:       "google/gemma-4-e2b",
    NotContinue: true,
}
  • IsNeedContinue() bool — возвращает true, если после tool calls loop остановлен
  • Continue() (*Message, error) — продолжает без добавления нового user-сообщения
  • ContinueStream(callback StreamCallback) (*Message, error) — стриминговая версия

Пример:

msg, err := api.ChatCompletions("Какая погода?")
// msg содержит tool_calls, needContinue = true

for api.IsNeedContinue() {
    // История уже содержит tool-результаты
    msg, err = api.Continue()
}

Callback'и жизненного цикла

Четыре callback'а позволяют наблюдать за внутренним циклом библиотеки:

Callback Когда вызывается
OnBeforeLLM(prompt) Перед каждым HTTP-запросом к LLM (включая повторные раунды)
OnAfterLLM(msg) После каждого успешного ответа от LLM
OnBeforeCallTool(call) Перед выполнением каждого инструмента
OnAfterCallTool(resp) После выполнения каждого инструмента

Все callback'и опциональны (nil-safe).

opts := &llm.LLMOpts{
    ...
    OnBeforeLLM: func(prompt string) {
        log.Printf("→ запрос к LLM: %s", prompt)
    },
    OnAfterLLM: func(msg *llm.Message) {
        log.Printf("← ответ получен (tool_calls: %d)", len(msg.ToolCalls))
    },
}

MaxParallelTools

По умолчанию инструменты выполняются последовательно. MaxParallelTools > 1 включает параллельное выполнение через пул горутин:

opts := &llm.LLMOpts{
    ...
    MaxParallelTools: 5, // до 5 инструментов одновременно
}
  • Результаты возвращаются в порядке вызовов (Promise.all)
  • Паника в инструменте возвращается как ошибка, остальные горутины не прерываются
  • Callback'и OnBeforeCallTool/OnAfterCallTool вызываются синхронно в главной горутине

Reasoning Content

Некоторые модели (DeepSeek-R1, QwQ, Gemma-4 и др.) возвращают цепочку рассуждений в отдельном поле reasoning_content. Библиотека парсит это поле в Message.Reasoning (non-streaming) и StreamChunk.Reasoning (streaming).

Поле Reasoning имеет json:"reasoning_content,omitempty" — при отправке истории обратно в API пустое рассуждение не попадает в messages[].

Non-streaming

msg, err := api.ChatCompletions("Сколько будет 2+2?")
fmt.Println("Ответ:", msg.Content)
fmt.Println("Рассуждение:", msg.Reasoning)

Streaming

response, err := api.ChatCompletionsStream("Сколько будет 2+2?", func(chunk llm.StreamChunk) {
    if chunk.Reasoning != "" {
        fmt.Printf("[рассуждает] %s\n", chunk.Reasoning)
    }
    if chunk.Content != "" {
        fmt.Print(chunk.Content)
    }
})

API не управляет рассуждением

Стандартный OpenAI-совместимый API (включая LM Studio) не имеет параметра для включения/отключения рассуждения. Это свойство модели:

  • Рассуждающие модели всегда рассуждают (архитектурно)
  • Модели без рассуждения никогда не возвращают reasoning_content
  • Message.Reasoning будет пустым для моделей без рассуждения

В LM Studio есть UI-галочка "When applicable, separate reasoning_content and content in API responses", которая управляет только вырезанием <think>-тегов в отдельное поле. Библиотека работает в обоих режимах.

Done: true приходит только один раз — в самом конце, после финального ответа.

Usage (токены)

После каждого запроса библиотека сохраняет информацию о расходе токенов. Данные берутся из ответа API (prompt_tokens, completion_tokens, total_tokens) и передаются в HistoryOpts.OnLimit.

Тестирование

# Unit-тесты (без внешних сервисов)
go test -short ./...

# Все тесты, включая интеграционные
go test ./...

Интеграционные тесты автоматически пропускаются, если сервисы недоступны.