Что даёт
Автоматическое ревью и описание merge request через PR-Agent (Qodo Merge), генерация changelog и сообщений коммитов из CI, подключение GitLab Duo Self-Hosted к нашим моделям.
Эндпоинт: POST https://api.morsmorii.ru/v1/chat/completions
Какую модель брать
ревью MR — claude-sonnet-5-5; описания MR и сообщения коммитов — claude-haiku-4-5; архитектурный разбор — claude-opus-5-5
Ревью требует понимания кода и осторожности — Sonnet 5.5. Описания и коммиты — короткие тексты по diff, Haiku делает их за копейки. Архитектурный разбор — редкая задача с большим контекстом, где качество Opus окупается.
Ориентир стоимости
ревью MR на 300 строк: ~20 000 токенов на входе и 2 000 на выходе — около 19 ₽; сообщение коммита или описание MR на Haiku 4.5: ~3 000 токенов на входе — около 1 ₽; архитектурный разбор на Opus 5.5: ~50 000 токенов на входе и 5 000 на выходе — около 90 ₽
Шаги
- GitLab Duo Self-Hosted (нужны GitLab Premium или Ultimate с Duo Enterprise и свой AI Gateway — лицензии у нас нет, поэтому только по документации):
- Admin Area → AI-powered features → Self-hosted models → Add self-hosted model.
- Platform: OpenAI-compatible; Endpoint: https://api.morsmorii.ru/v1; Model identifier: custom_openai/claude-sonnet-5-5; API key: ключ Morsmorii.
- Затем в Features назначьте модель на Code Suggestions (generation), Duo Chat и Merge request summary; для summary можно завести вторую модель custom_openai/claude-haiku-4-5.
- Проверка с их стороны: Admin → Self-hosted models → Test model — это обычный chat/completions к нам.
Конфигурация
Вместо sk-ВАШ_КЛЮЧ_MORSMORII подставьте ключ из раздела «API-ключи». Для инструмента лучше завести отдельный ключ с ограничением по моделям и бюджетом.
# Переменные проекта (Settings → CI/CD → Variables, masked):
# MORSMORII_API_KEY — ключ Morsmorii (ограничьте его claude-sonnet-5-5 и claude-haiku-4-5)
# GITLAB_PAT — personal access token с правами api (комментарии в MR)
pr_agent:
stage: test
image:
name: codiumai/pr-agent:latest
entrypoint: [""] # у образа свой ENTRYPOINT, а runner'у GitLab нужен shell
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
variables:
CONFIG__GIT_PROVIDER: gitlab
GITLAB__URL: $CI_SERVER_URL
GITLAB__PERSONAL_ACCESS_TOKEN: $GITLAB_PAT
OPENAI__KEY: $MORSMORII_API_KEY
OPENAI__API_BASE: https://api.morsmorii.ru/v1
CONFIG__MODEL: openai/claude-sonnet-5-5
CONFIG__FALLBACK_MODELS: '["openai/claude-haiku-4-5"]'
CONFIG__CUSTOM_MODEL_MAX_TOKENS: "200000"
PR_DESCRIPTION__MODEL: openai/claude-haiku-4-5
script:
- cd /app # код PR-Agent в образе лежит здесь
- MR_URL="$CI_SERVER_URL/$CI_PROJECT_PATH/-/merge_requests/$CI_MERGE_REQUEST_IID"
- python -m pr_agent.cli --pr_url "$MR_URL" describe
- python -m pr_agent.cli --pr_url "$MR_URL" review
- python -m pr_agent.cli --pr_url "$MR_URL" improvedocker run -d --name pr-agent -p 3000:3000 \ -e CONFIG__GIT_PROVIDER=gitlab \ -e GITLAB__URL=https://gitlab.example.com \ -e GITLAB__PERSONAL_ACCESS_TOKEN=glpat-... \ -e GITLAB__SHARED_SECRET=любая_строка \ -e OPENAI__KEY=sk-ВАШ_КЛЮЧ_MORSMORII \ -e OPENAI__API_BASE=https://api.morsmorii.ru/v1 \ -e CONFIG__MODEL=openai/claude-sonnet-5-5 \ -e CONFIG__CUSTOM_MODEL_MAX_TOKENS=200000 \ -w /app --entrypoint python \ codiumai/pr-agent:latest -m pr_agent.servers.gitlab_webhook # В проекте: Settings → Webhooks → URL http://ваш-хост:3000/webhook, # Secret token = GITLAB__SHARED_SECRET, события: Merge request, Comments.
changelog:
stage: deploy
image: alpine:3.20
rules:
- if: $CI_COMMIT_TAG
variables:
GIT_DEPTH: "0" # полная история: иначе git describe не найдёт прошлый тег
before_script:
- apk add --no-cache curl jq git
script:
- PREV=$(git describe --tags --abbrev=0 "$CI_COMMIT_TAG"^ 2>/dev/null || git rev-list --max-parents=0 HEAD)
- LOG=$(git log --no-merges --pretty='- %s (%h)' "$PREV".."$CI_COMMIT_TAG")
- |
jq -n --arg log "$LOG" --arg tag "$CI_COMMIT_TAG" '{
model: "claude-haiku-4-5",
messages: [
{role: "system", content: "Ты пишешь changelog для релиза. Сгруппируй изменения: Новое, Исправления, Внутреннее. Кратко, по-русски, markdown."},
{role: "user", content: ("Релиз " + $tag + "\n\nКоммиты:\n" + $log)}
]}' > req.json
- |
curl -sS --fail https://api.morsmorii.ru/v1/chat/completions -H "Authorization: Bearer $MORSMORII_API_KEY" \
-H "Content-Type: application/json" -d @req.json | jq -r '.choices[0].message.content' > CHANGELOG-$CI_COMMIT_TAG.md
- cat CHANGELOG-$CI_COMMIT_TAG.md
artifacts:
paths: [CHANGELOG-$CI_COMMIT_TAG.md]mr_summary:
stage: test
image: alpine:3.20
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
variables:
GIT_DEPTH: "0" # при глубине 20 git diff A...B отвечает «no merge base»
before_script:
- apk add --no-cache curl jq git
script:
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --depth=200
- DIFF=$(git diff "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"...HEAD | head -c 60000)
- |
jq -n --arg diff "$DIFF" '{
model: "claude-haiku-4-5",
messages: [
{role: "system", content: "Опиши merge request по diff: что меняется, зачем, на что смотреть ревьюеру. По-русски, до 10 строк, markdown."},
{role: "user", content: $diff}
]}' > req.json
- |
SUMMARY=$(curl -sS --fail https://api.morsmorii.ru/v1/chat/completions -H "Authorization: Bearer $MORSMORII_API_KEY" \
-H "Content-Type: application/json" -d @req.json | jq -r '.choices[0].message.content')
- |
jq -n --arg body "$SUMMARY" '{body: $body}' | curl -sS -X POST \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" \
-H "PRIVATE-TOKEN: $GITLAB_PAT" -H "Content-Type: application/json" -d @-#!/bin/sh
# $1 — файл сообщения; $2 — источник (пусто при обычном git commit, тогда и пишем)
[ -n "$2" ] && exit 0
[ -z "$MORSMORII_API_KEY" ] && exit 0
MSG=$(git diff --cached | head -c 30000 | jq -Rs '{model: "claude-haiku-4-5", messages: [
{role: "system", content: "Напиши сообщение коммита по diff: заголовок до 72 знаков, пустая строка, 1-3 строки сути. По-русски."},
{role: "user", content: .}]}' \
| curl -sS --fail https://api.morsmorii.ru/v1/chat/completions \
-H "Authorization: Bearer $MORSMORII_API_KEY" -H "Content-Type: application/json" -d @- \
| jq -r '.choices[0].message.content // empty')
[ -n "$MSG" ] || exit 0
{ printf '%s\n\n' "$MSG"; cat "$1"; } > "$1.tmp" && mv "$1.tmp" "$1"export MORSMORII_API_KEY="sk-ВАШ_КЛЮЧ_MORSMORII" # или в ~/.bashrc git add -A git commit # редактор откроется с сообщением от Haiku — поправьте и сохраните # git commit -m "..." хук не трогает: сообщение уже задано
Как ограничить ключ
- В разделе «API-ключи» создайте отдельный ключ для этого инструмента — так расход виден отдельно, а отзыв не заденет остальное.
- В поле «Модели» оставьте только рекомендованные выше: ключ ответит 403 на любую другую модель.
- Задайте месячный бюджет в рублях: после него — 429 key_budget_exceeded, списание останавливается.
- В команде каждый сотрудник заводит свои ключи: расход по людям видно в разделе «Команда» и в
GET /v1/usage.
Тонкости
- Ключ для CI ограничьте моделями claude-sonnet-5-5 и claude-haiku-4-5 и месячным бюджетом (например, 2 000 ₽) — расход по ключу виден в /developers и в GET /v1/usage.
- PR-Agent ходит через litellm: префикс openai/ обязателен, а CONFIG__CUSTOM_MODEL_MAX_TOKENS нужен, потому что наши id ему неизвестны.
- Запуск PR-Agent в CI (entrypoint: [""] и cd /app) и как вебхук (python -m pr_agent.servers.gitlab_webhook) — по документации PR-Agent; проверьте тег образа на Docker Hub: у PR-Agent бывает отдельный тег для вебхука (codiumai/pr-agent:gitlab_webhook), тогда --entrypoint и команду после образа можно убрать.