DSH Plugins Marketplace

DSH Plugins

Plugins

/

Models & Providers

/

dsh-rustvy

d

dsh-rustvy

Manifest valid

Vendor marks in the model picker plus a model list kept in step with the provider: mirrors the public catalog and logos to loopback, and regenerates the pi-ai route's models from that route's own endp

UI (client)hasBundlePatch

Регистрация на Rustvy по реферальной ссылке автора: https://rustvy.xyz/r/Z6LJC8FH При регистрации по этой ссылке вы получаете скидку 10% и бонусные токены. Подробные условия акции: https://rustvy.xyz/dashboard/referral

Rustvy (dsh-rustvy)

Русский | English

npm загрузки лицензия dsh-plugin

Значки производителей в списке моделей DeepSeek Harness, список моделей, который остаётся настолько же актуальным, насколько актуален сам провайдер, и несколько ключей провайдера в одном профиле — каждый чат отправляет запросы тем, который выбрал. Один плагин, одно зеркало, без перезапусков.

Что он делает

У плагина три задачи. Первые две работают от одного зеркала публичного каталога провайдера; третьей зеркало не нужно — ей нужен только маршрут профиля.

Значки. Один монохромный значок производителя перед каждой строкой модели в списке выбора — нарисованный так же, как его рисует собственная панель провайдера: CSS-маска, заливаемая currentColor, поэтому один файл на производителя обслуживает обе темы оформления и все состояния строки — наведение, выбор, отключение — без цветовых вариантов.

ГдеЧто появляется
Меню моделей в композеремонохромный значок 16 px перед каждой строкой модели
Кнопка модели в композерезначок выбранной модели перед её названием (14 px)
Всплывающее окно команды /modelтот же значок на каждой строке
Скины, перерисовывающие список (dsh-claude-style)тот же значок; собственная плашка скина в этой строке прячется, чтобы две плашки не встретились

Список моделей. Профиль подключается к стороннему провайдеру через вручную объявленный маршрут @deepseek-ai/dsh-llm-pi-ai, и именно список models этого маршрута определяет, что вообще можно выбрать в интерфейсе: harness предлагает модель только тогда, когда её отдаёт маршрут. Список, написанный вручную, устаревает в тот день, когда провайдер выпускает новую модель — новая невидима, а снятая с публикации остаётся выбираемой и падает уже на запросе. Плагин читает, что отдаёт собственный эндпоинт маршрута, превращает это в точный формат записей адаптера, показывает расхождение с установленным списком и по команде записывает его обратно через ctx.configEditor.

Ключи. Несколько ключей провайдера в одном профиле и свой ключ для отдельного чата — выбираемый в панели инструментов композера, сразу за списком режимов доступа. Ключ добавляется в самом плагине, кнопкой «+»: ни .env, ни cordis.patch.yml, ни страницу Models трогать не нужно, а выбор переживает перезапуск. Учётная запись DeepSeek не затрагивается: подмена касается только маршрутов этого плагина.

ГдеЧто появляется
Настройки → Rustvyраздел «API-ключи»: список ключей, кнопка «+», замена значения, переименование, проверка, назначение ключом по умолчанию, удаление
Композер, сразу за списком режимов доступакомпактный переключатель ключа этого чата, с меню и строкой «+ Добавить ключ»

Раздела ключей в меню моделей и в окне /model нет: выбор ключа живёт в одном месте. Меню моделей вместо этого не показывает те группы провайдеров, которые оказались клонами маршрутов этого кольца, — иначе один и тот же список моделей повторялся бы по разу на каждый ключ.

Зачем это нужно

Потому что обе поломки маршрута, который поддерживают вручную, — тихие. Модель, которую провайдер добавил, не появляется, и никто не говорит почему: ошибки нет, есть только отсутствие. Модель, которую провайдер снял, по-прежнему видна, и её выбор даёт сетевую ошибку, которая выглядит как проблема со связью, а не как устаревшая строка в YAML-файле.

И то и другое лечится чтением данных у провайдера вместо их переписывания. Список, который записывает этот плагин, несёт настоящие размер окна контекста, лимит вывода, входные модальности и уровни рассуждения, поэтому модель, получившая поддержку изображений или новый уровень мышления, описана верно в тот же день, а не наследует запасное значение, которое кто-то когда-то угадал.

А поскольку значки берутся из того же зеркала, совершенно новая модель приходит уже с кешированным значком своего производителя: зеркало перечитывает каталог в тот момент, когда эндпоинт называет модель, которой оно раньше не видело, — то есть ровно тогда, когда выходит релиз.

Третья задача — про другую тишину. Ключ провайдера — это отдельный лимит и отдельный учёт, а профиль один, поэтому «свой ключ на каждый чат» без плагина означает либо второй профиль, либо вторую строку в YAML, которую кто-то должен поддерживать вручную. Вручную поддержанный клон маршрута устаревает по тем же правилам, что и написанный вручную список моделей, — в день релиза. Кольцо ключей пишет клоны само и тем же проходом, которым записывается список моделей, поэтому маршрут ключа не отстаёт от маршрута профиля, а чат, отправляющий запросы другим ключом, видит ровно тот же выбор моделей, что и его сосед.

Ничего из этого не привязано к одному провайдеру. Сайт-источник, маршрут и строка профиля — настройки; https://rustvy.xyz и rustvy являются значениями по умолчанию.

Установка

dsh plugin --profile web add dsh-rustvy

Либо прямо с GitHub, если нужна самая свежая ревизия, а не опубликованная версия:

dsh plugin --profile web add github:koliy82/dsh-rustvy

Голый koliy82/dsh-rustvy без схемы не принимается: разбор источника установки знает имя в реестре, git-спецификацию, tarball и абсолютный локальный путь, а git-спецификации нужна схема либо сокращение github:.

Подтверждение сборки не запрашивается ни в том ни в другом случае: у пакета нет ни install-скрипта, ни зависимостей, поэтому pnpm нечего собирать.

Из локальной копии этого репозитория:

pwsh -File .\install.ps1

install.ps1 копирует пакет в <профиль>\node_modules\dsh-rustvy как настоящий каталог и дописывает имя в dsh.profile.bundles манифеста профиля. Работающий harness подхватывает строку по изменению манифеста.

После этого один раз перезапустите harness или обновите страницу: значки и страница настроек появляются на лету, а строка загрузчика читается на композиции.

Маршрут, в который пишется список моделей

Значки работают без всего остального. Половине со списком моделей нужен маршрут @deepseek-ai/dsh-llm-pi-ai, в который можно писать. Добавьте в cordis.patch.yml профиля — или отредактируйте в Настройки → Плагины → Конфигурация плагинов:

- id: llm-pi-ai
  name: "@deepseek-ai/dsh-llm-pi-ai"
  config:
    providers:
      rustvy:
        displayName: rustvy
        apiKeyEnv: RUSTVY_API_KEY
        api: openai-completions
        baseURL: https://rustvy.xyz/v1
        models:
          - id: placeholder
            name: placeholder

Список models обязан только разбираться: этот плагин его заменяет. Все остальные поля при перегенерации переносятся без изменений, потому что переключатель совместимости протокола или ссылка на учётные данные — это знание о вашем развёртывании, которого нет ни в одном каталоге.

apiKeyEnv задаёт, откуда маршрут берёт ключ; он разрешается на каждый запрос через механизм учётных данных harness — сначала унаследованное окружение процесса, затем $DSH_HOME/.credentials.yaml, затем .env в каталоге запуска или в $DSH_HOME. Сам ключ в конфигурационные файлы не попадает. Плагин при чтении списка моделей подставляет ключ только тогда, когда окружение процесса уже содержит переменную с таким именем; эндпоинт, под который он написан, отвечает и без ключа.

Чтобы сделать модель основной:

- id: agent-default-model
  name: "@deepseek-ai/dsh-agent-default-model"
  config:
    provider: rustvy
    model: claude-opus-5-5
    reasoningEffort: high

Как пользоваться

Откройте Настройки → Rustvy. Страница показывает, что держит зеркало, что отдаёт провайдер и каково точное расхождение между ними. Кнопка «Обновить каталог и иконки» освежает обе половины, «Записать список моделей» записывает сгенерированный список. Без одного из этих нажатий не пишется ничего, если вы сами не попросили об этом в настройках.

Раздел «API-ключи» на той же странице держит кольцо: кнопка «+» открывает инлайн-форму «название + значение», у каждой строки ключа есть замена значения, переименование, назначение ключом по умолчанию, проверка и удаление. Ключом по умолчанию можно пользоваться, вообще не заходя в настройки: переключатель в панели инструментов композера делает то же самое для того чата, который открыт.

Настройки

Настройки → Rustvy

Значки:

ПараметрПо умолчаниюСмысл
Показывать иконки моделейвклобщий выключатель значков; в выключенном виде убирает все значки и не трогает список моделей
Иконка в кнопке выбранной моделивклзначок и в кнопке модели композера, а не только в строках меню
Прятать логотипы других скиноввклпрятать собственную плашку скина в строке, где значок нарисован этим плагином
Размер значка, px16от 10 до 32

Список моделей:

ПараметрПо умолчаниюСмысл
Держать список моделей маршрута актуальнымвклобщий выключатель; в выключенном виде YAML маршрута не трогается
Записывать списокпри нажатии «Обновить»только отдельной кнопкой — самостоятельно не пишет никогда · при нажатии «Обновить» — пишет по кнопке обновления · сам, при каждом обновлении — пишет и при фоновом суточном обновлении
Название моделикак на сайте провайдераотображаемое имя провайдера либо сырой идентификатор
Предлагать уровень «без рассуждений»вклдобавлять уровень off рассуждающей модели, которая его не объявила
Оставлять модели, которые провайдер больше не отдаётвыклсохранять установленную модель, которую эндпоинт больше не отдаёт, — на случай временно ошибочного списка, а не действительно снятой модели
Маршрут провайдераrustvyкакой ключ внутри providers записывать
Строка профиляllm-pi-aiкакая строка загрузчика его содержит
Сайт провайдераhttps://rustvy.xyzоткуда читать каталог и логотипы

Режим записи по умолчанию намеренно средний: нажатие кнопки — это решение человека, а фоновое истечение срока — нет, и переписывать конфигурацию загрузчика работающего профиля, когда никто не просил, — это то, на что стоит спросить разрешение.

Каждый адрес и файл, с которым работает плагин, виден на странице: сайт, страница каталога и эндпоинт списка моделей — ссылками, открывающимися в новой вкладке, а пути к зеркалу, настройкам, копии для отката и патчу профиля — текстом с кнопкой копирования. Кнопка «Показать блок для ручной правки» выдаёт ровно тот YAML, который получился бы при автоматической записи, так что желающий вставить его в cordis.patch.yml сам не переписывает краткий пересказ.

У одного поля нет элемента управления, потому что оно принадлежит YAML профиля, а не форме: modelZones (по умолчанию ["standard"]) — зоны каталога, которые допускаются, когда эндпоинт недоступен и состав списка определяет зеркало.

Ключи:

ПараметрПо умолчаниюСмысл
Ключи на несколько чатоввклобщий выключатель кольца; в выключенном виде подмена маршрута не применяется и каждый запрос идёт маршрутом профиля

Выключение ничего не удаляет: маршруты ключей остаются в конфигурации профиля, значения — в хранилище учётных данных, выбор чатов — в своём файле, поэтому включённая обратно возможность продолжает с того места, на котором её выключили. Переключатель живёт в разделе «API-ключи», а не рядом с выключателями значков и списка моделей: он принадлежит кольцу, и там же страница объясняет, почему добавление ключа недоступно, если хост не отдаёт нужный сервис.

Настройки хранятся в <профиль>\.dsh-rustvy\config.json. Две переменные окружения перекрывают их — для развёртывания, которое направляет зеркало в другое место или хочет смонтировать плагин неработающим: DSH_RUSTVY_ORIGIN и DSH_RUSTVY_DISABLED=1.

Откуда берутся картинки

В сам пакет ничего не вшито. Хост-половина зеркалит два публичных объекта с сайта провайдера в профиль, а браузерная половина читает их с локального адреса:

  1. каталог моделей — сайт рисует свою таблицу моделей из JSON-острова в собственной странице (<script id="rustvy-catalog">), поэтому неаутентифицированный читатель получает ровно те данные, которые видит панель. Зеркалится в <профиль>\.dsh-rustvy\catalog.json.
  2. файлы логотипов (/logos/<имя>.svg) — копируются в <профиль>\.dsh-rustvy\logos\ при первом обращении.

Оба читаются по собственному исходящему маршруту процесса (lib/net.js): сначала живой глобальный диспетчер, чтобы прокси, установленный dsh-proxy-gui или любым другим маршрутом процесса, соблюдался, затем соединитель, построенный здесь из HTTPS_PROXY / ALL_PROXY / HTTP_PROXY, — на случай, когда диспетчер не установлен. NO_PROXY учитывается в обоих случаях. Сама страница наружу не выходит: браузер спрашивает значок у 127.0.0.1, и именно поэтому всё работает на машине, чей браузер до источника не достаёт вовсе.

Каталог обновляется раз в 24 часа и по требованию; холодное зеркало ожидается до 6 секунд, а устаревшее обновляется в фоне, пока строки продолжают показывать имеющиеся значки. Когда источник недоступен, кеш на диске по-прежнему отдаёт всё, что в нём есть.

Откуда берётся список моделей

Два источника в таком порядке, потому что отвечают они на разные вопросы:

  1. собственный эндпоинт маршрута (GET {baseURL}/models) — авторитетный список состава: ровно те модели, которые можно назвать в запросе, каждая со своими context_k, max_output_tokens, modalities и reasoning_levels. Это тот же эндпоинт, который опрашивает собственное обнаружение адаптера.
  2. зеркало острова каталога — названия и запасной вариант. Это единственный источник, несущий отображаемое имя, производителя и имя файла логотипа, поэтому он соединяется с отдаваемым списком по идентификатору. Когда эндпоинт не отвечает, состав определяет сам остров по своим полям enabled и zone, так что маршрут остаётся актуальным и во время сбоя, а не застывает на последнем удачном чтении. На здоровом источнике оба списка совпадают точно.

Модель, которую эндпоинт отдаёт, а зеркало ни разу не видело, — признак того, что остров отстал от боевого API, а это происходит ровно в момент релиза, поэтому тогда остров перечитывается; ожидание ограничено тем же коротким сроком, что и для холодного зеркала.

Что именно записывается

Каждая сгенерированная запись несёт только те поля, которые объявлены в схеме конфигурации адаптера, потому что список models профиля маршрута заменяет каталог маршрута, а неизвестное поле в лучшем случае будет отброшено:

ПолеОткуда берётся
idидентификатор отдаваемой модели — то, что нужно назвать в запросе
nameотображаемое имя из острова либо идентификатор, если имени нет
contextWindowcontext_k × 1000
maxTokensmax_output_tokens
inputmodalities, отфильтрованные до text/image
reasoningEffortsreasoning_levels, отображённые на уровни адаптера, либо false для нерассуждающей модели
compatне генерируется никогда — существующее всегда переносится

Поле, о котором никто не высказался, пропускается, а не заполняется значением по умолчанию, чтобы запасное значение самого адаптера оставалось в силе и написанное вручную не закреплялось молча. compat — единственное, что переносится без изменений: переключатель совместимости протокола это знание о шлюзе, которого нет ни в одном каталоге.

Уровни рассуждения отображаются на словарь адаптера (off, minimal, low, medium, high, xhigh, max); неизвестный ему уровень отбрасывается и сообщается примечанием, а не отказом всей записи. Необъявленные уровни адаптер не поддерживает, а объявленный off без боевого значения означает «предлагать, ничего не отправляя» — поэтому этот переключатель включён по умолчанию.

Что запись отклоняет

Пустой список не записывается никогда: единственный способ его получить — неудачное чтение, а маршрут без моделей не обслуживает ничего. Актуальный список тоже не записывается, потому что configEditor.edit() согласует весь загрузчик, и тратить одно согласование на отсутствие изменений означало бы перезагрузить работающий профиль впустую. Актуальным при этом считается не только маршрут профиля: запись происходит и тогда, когда сам он в порядке, а список моделей одного из клонов кольца ключей отстал, — иначе чат, отправляющий запросы другим ключом, видел бы меньше моделей, чем его сосед. Каждая запись сохраняет предыдущий документ в <профиль>\.dsh-rustvy\models-backup.json, а кнопка «Откатить к предыдущему» возвращает его обратно; состояние, которое откат вытесняет, само становится следующей копией, поэтому откат тоже можно откатить.

Как строка сопоставляется со значком

Строки сопоставляются по видимому названию в таком порядке:

  1. идентификатор модели из зеркального каталога (claude-opus-5-5, abyss/glm-5.3, …);
  2. её отображаемое имя (OPUS 5.5);
  3. идентификатор, с которого название начинается, — скин может оставить в том же элементе собственную строку описания, поэтому название часто является префиксом, а не всей строкой; побеждает самый длинный подходящий идентификатор, чтобы claude-opus-5-5 никогда не достался claude-opus-5;
  4. форма идентификатора — для модели, которой зеркало ни разу не видело: claude-* → Anthropic, gpt-*/o1…o9 → OpenAI, glm-* → Zhipu, kimi-*/k3 → Moonshot, qwen-* → Alibaba Qwen, deepseek-* → DeepSeek, ahmia-* → Ahmia, mimo-* → Xiaomi, fable-* → Anthropic, gemini-* → Google, grok-* → xAI.

Модель, не подошедшая ни под что, не получает значка вовсе: выдуманная иконка была бы неправильным ответом, а не отсутствующим. Модель, которая подошла, но чей логотип зеркало получить не смогло, получает вместо пустого квадрата плашку с монограммой — первыми буквами производителя: маска с неработающим URL закрасилась бы сплошным квадратом.

Строки уровней усилия, заголовки производителей, корневые ячейки меню, поле поиска и строки о сбое каталога не подходят ни под одно правило и остаются без значка.

Кольцо API-ключей

Несколько ключей провайдера в одном профиле, и каждый чат отправляет запросы тем, который выбрал его пользователь. Выбор живёт в панели инструментов композера — рядом со списком режимов доступа, а не в меню моделей, — действует на один чат и переживает перезапуск. Учётная запись DeepSeek не затрагивается ни в одном из мест этого раздела.

Почему один ключ — это один маршрут

@deepseek-ai/dsh-llm-pi-ai берёт ключ из profile.apiKeyEnv — это имя, а не значение, — и разрешает его через механизм учётных данных harness на каждый запрос, ничего не кешируя. Поля для ключа нет ни в конфигурации вызова (LlmCallConfig), ни в параметрах генерации (GenerateOptions), а baseURL и заголовки запекаются в снапшот профиля. Передать адаптеру ключ «в этом запросе» нечем, поэтому единственный честный способ дать чату другой ключ — другой маршрут: клон с теми же api, baseURL и models и своим apiKeyEnv.

Из этого следует всё устройство. Добавление ключа пишет значение в хранилище учётных данных под новым именем, клонирует маршрут профиля через ctx.configEditor и запоминает запись в кольце. Выбор чата применяется на водопаде agent/request подменой имени маршрута в конфигурации вызова — тем же приёмом и на том же шве, которым harness применяет выбор модели для отдельного чата, поэтому он работает ровно там, где уже работает выбор модели, и не заводит собственного пути к запросу.

КлючИмя учётных данныхМаршрутОтображаемое имя маршрута
первый (основной)RUSTVY_API_KEY — как объявлен в профилеrustvy — как объявлен в профилеrustvy
второйRUSTVY_API_KEY_2rustvy-k2rustvy · <название ключа>
n-йRUSTVY_API_KEY_<n>rustvy-k<n>rustvy · <название ключа>

Имена строятся от маршрута из настроек, а не зашиты: при modelRoute: rustvy второй ключ получает rustvy-k2, при другом имени маршрута — свои суффиксы. Клон копирует профиль маршрута целиком, меняя только apiKeyEnv и отображаемое имя, поэтому написанный вручную переключатель совместимости протокола и всё остальное знание о своём шлюзе переходит в каждый ключ, а не теряется при первом же добавлении.

Предел — 12 ключей: это кольцо, а не реестр. Новый ключ берёт первый порядковый номер, у которого свободны и имя учётных данных, и маршрут, поэтому имя удалённого ключа не переиспользуется, а чужая строка, случайно оказавшаяся на таком имени, не перезаписывается.

Основной ключ

Первый ключ кольца — не запись, которую кто-то добавил, а маршрут, уже объявленный в профиле: он наследует его apiKeyEnv и его имя и основным остаётся всегда. Профиль, который работал до этой возможности, продолжает работать без изменений — кольцо с одним ключом не переименовывает ничего и никуда не пишет. Удалить основной ключ нельзя: он принадлежит маршруту профиля, и его удаление сломало бы провайдера для каждого чата, включая те, которые кольцом ни разу не воспользовались. Отказ так и объясняет и предлагает заменить значение — эта операция доступна любому ключу и конфигурации не касается, потому что имя учётных данных при ней не меняется.

Где что лежит

ЧтоГде
Значения ключей$DSH_HOME\.credentials.yaml, раздел refs — то же хранилище, в которое пишет страница Models harness и из которого читает адаптер
Метаданные кольца<профиль>\.dsh-rustvy\keys.json — id, название, имя учётных данных, маршрут, признак основного, порядковый номер, дата создания и defaultKeyId
Выбор чатов<профиль>\.dsh-rustvy\session-keys.json — sessionId → keyId

В файлах плагина значения ключа нет никогда: ни в keys.json, ни в session-keys.json, ни в настройках, ни в копии профиля для отката. Loopback-маршруты отвечают маской — первые три и последние два символа — и восьмисимвольным отпечатком SHA-256; этого достаточно, чтобы узнать ключ в списке и заметить две записи с одним значением, и недостаточно, чтобы им воспользоваться. Само значение приходит в теле единственного запроса, который его добавляет или заменяет, и никогда в строке запроса: URL пишется в журналы, попадает в кеш и в историю браузера, а секрет — не дело ни того, ни другого, ни третьего.

Выбор хранится на диске, а не в журнале сессии: это предпочтение маршрутизации самого плагина, и плагин, пишущий собственные события в долговременный журнал, оставил бы их в каждой сессии, где когда-либо был смонтирован. В файле лежат только отклонения от ключа по умолчанию, поэтому чат, созданный до смены ключа по умолчанию, следует за новым, а не держится старого.

Что видно в интерфейсе

Две поверхности: одна управляет кольцом, другая выбирает ключ для своего чата. Переключатель появляется только тогда, когда выбирать есть из чего: при одном ключе его нет вовсе, потому что один ключ — это не выбор.

Настройки → Rustvy → «API-ключи». Список ключей: название, маска, имя учётных данных, маршрут, сколько моделей отдаёт этот маршрут и сколько чатов выбрали этот ключ. Кнопка «+» в заголовке раздела открывает инлайн-форму «название + значение» со скрытым полем и показом по желанию. У каждой строки — замена значения, переименование (сохраняется по потере фокуса), назначение ключом по умолчанию, проверка и удаление в два шага; у основного ключа кнопки удаления нет, и строка объясняет почему. Ниже — блок «Маршруты ключей»: строка профиля и пакет, адрес списка, счётчик «запросов через подмену», число чатов со своим выбором, списки отсутствующих и отставших маршрутов, пути к обоим файлам с кнопкой копирования и кнопка «Починить маршруты», которая появляется только тогда, когда маршрут какого-то ключа действительно пропал.

Композер. Компактная кнопка в панели инструментов, сразу за списком режимов доступа, — регистрация в слоте conversation.input.left: название активного ключа либо «ключ по умолчанию», если у чата нет собственного выбора. В меню — по строке на ключ с галочкой на активном, строка «По умолчанию» и строка «+ Добавить ключ» с той же инлайн-формой; ключ, добавленный из чата, сразу выбирается для этого чата, потому что добавляют его из разговора именно затем, чтобы отправлять запросы им. Кнопка не рисуется, пока слот не передал sessionId: выбор принадлежит чату, и переключатель, который сохранит его не тому чату, хуже отсутствия переключателя.

Меню выбора модели и всплывающее окно /model. Раздела ключей здесь нет — ни строк, ни заголовка: выбор живёт в композере, и второй такой же список в меню только удваивал бы место, где можно ошибиться. Декоратор делает здесь обратное — прячет группы провайдеров, которые хост назвал в hiddenGroups, то есть клоны маршрутов этого кольца, чтобы один и тот же список моделей не показывался по разу на каждый ключ. Не объявить их хост не может: клону нужен собственный список моделей, иначе после подмены маршрута запрос не найдёт в нём свою модель. Скрытие структурное и осторожное: заголовок группы опознаётся по форме, а не по имени класса, потому что классы хозяйских примитивов этому плагину не видны и между версиями не стабильны, а прячется либо наименьший содержащий группу элемент, либо заголовок вместе с идущими за ним строками до следующего заголовка. Если уверенности нет, не прячется ничего: по ошибке спрятанная строка модели — это отнятый выбор, а лишний раз показанная группа — только повтор.

Что происходит при добавлении ключа

Порядок операций намеренный. Сначала значение пишется в хранилище учётных данных: маршрут, чьё имя ни во что не разрешается, роняет каждый запрос, который его назвал. Затем идёт запись конфигурации, и если она не удалась, значение забирается обратно, поэтому неудачное добавление не оставляет после себя ничего. Документ кольца пишется последним, когда существуют и значение, и маршрут.

Запись конфигурации — настоящая и поддерживаемая: она идёт через ctx.configEditor, который согласует профиль, и ровно то же самое делает страница Models harness, когда в профиль добавляют провайдер. configEditor.edit() согласует весь загрузчик, поэтому записи кольца выстроены в очередь: два ключа, добавленные одновременно, — это две последовательные правки, а не две правки против документа, который одна другая только что заменила. И когда карта маршрутов уже соответствует кольцу, запись не происходит вовсе — тратить согласование на отсутствие изменений этот плагин отказывается и в списке моделей.

Ключ можно проверить, не отправляя им запрос: проверка делает GET {baseURL}/models с этим ключом и сообщает статус, число отданных моделей и текст ошибки провайдера как он пришёл.

Насколько узка подмена

Подмена меняет ровно одно поле конфигурации вызова — имя маршрута — и только когда выполнено всё перечисленное:

  1. возможность включена настройкой apiKeys;
  2. запрос пришёл с идентификатором сессии — без него выбирать не для кого;
  3. в кольце два ключа или больше;
  4. ключ этого чата — его собственный выбор, а при его отсутствии ключ по умолчанию — не основной;
  5. запрос и без того шёл одним из маршрутов этого кольца (rustvy, rustvy-k*);
  6. целевой маршрут установлен в профиле;
  7. целевой маршрут отдаёт запрашиваемую модель.

Во всех остальных случаях конфигурация возвращается как пришла. deepseek-account и любой другой провайдер — второй шлюз, соседняя строка, чужая запись с ключом — не переименовываются никогда: условие пятое проверяет принадлежность маршрута кольцу, а не его похожесть.

Клон, который пропал или не отдаёт нужную модель, не роняет шаг: запрос уходит маршрутом из профиля, а причина записывается. Она видна в поле fallback на GET /keys — страница настроек рисует её предупреждением над списком ключей — и строкой в журнале, которая не повторяется, пока причина не изменится. Логика здесь такая: неработающий маршрут ключа — это проблема конфигурации, и правильный ответ на неё — состоявшийся запрос плюс видимая причина, а не упавший шаг, о котором не сказано почему. Исключение внутри самой подмены перехватывается, и запрос идёт как шёл.

Список моделей пишется одним проходом на все маршруты кольца — базовый и каждый клон, который кольцо видит в профиле, — поэтому клон не может отстать от базового маршрута, а блок для ручной правки (/models.yaml) содержит клоны вместе с их именами учётных данных, чтобы желающий вставить строку в cordis.patch.yml сам не реконструировал их по описанию.

Два счётчика на GET /keys отвечают на вопрос, который иначе выглядел бы как «ключи не работают»: routing.seen — сколько запросов harness вообще провёл через водопад agent/request, routing.applied — сколько из них были переименованы на маршрут ключа. Ноль при живом трафике означает, что водопад не доходит до плагина, и это видно сразу.

Маршруты

Одна регистрация префикса на собственном веб-сервере harness. Маршруты плагинов не видны фильтру запросов хоста, поэтому каждый из них проверяет собеседника сам: только локальный адрес, только локальный Host, только same-origin Sec-Fetch-Site и только совпадающий Origin.

МаршрутМетодОтвет
/dsh-rustvy/catalog.jsonGETзеркальный каталог плюс настройки, с которыми браузерная половина рисует
/dsh-rustvy/logo/<имя>.svgGET, HEADодин кешированный логотип, при первом запросе забираемый с источника; 404, когда такого значка у источника нет, 400 для любого имени, не совпадающего с [A-Za-z0-9._-]+.svg
/dsh-rustvy/models.jsonGETсгенерированный список моделей и его расхождение с установленным; ?cached=1 отдаёт последний вычисленный план вместо повторного чтения эндпоинта
/dsh-rustvy/models.yamlGET, HEADтот же список в виде готовой к вставке строки cordis.patch.yml; ?cached=1 как выше
/dsh-rustvy/stateGETсостояние зеркала: версия, число моделей, кешированные логотипы, маршрут, прокси (учётные данные маскируются), все пути к файлам, последняя ошибка, последнее вычисленное расхождение и сводка кольца ключей. Собственных запросов не делает; сводка кольца синхронная и без чтения учётных данных, поэтому масок в ней нет — только счётчики и маршруты
/dsh-rustvy/refreshPOSTперечитать каталог, заново прогреть все названные им логотипы и сообщить — либо, когда режим записи позволяет, применить — список моделей
/dsh-rustvy/apply-modelsPOSTзаписать сгенерированный список в профиль маршрута через ctx.configEditor
/dsh-rustvy/revert-modelsPOSTвернуть сохранённый профиль маршрута
/dsh-rustvy/configPOSTзаписать настройки; сети не касается
/dsh-rustvy/keysGET, POSTGET: кольцо целиком — ключи с маской и отпечатком, ключ по умолчанию, скрытые группы провайдеров, отсутствующие и отставшие маршруты, счётчики подмены, последний отказ, пути к обоим файлам, доступность credentials и configEditor. POST: одно действие над кольцом (add, set, rename, default, remove, repair, test); успешный ответ — свежий GET плюс поле результата, поэтому один POST перерисовывает всё. Отказ — 409 с { ok: false, error }, неизвестное действие — 400. Значение ключа принимается только в теле запроса
/dsh-rustvy/session-keyGET, POSTGET с ?sessionId=: каким ключом этот чат отправляет запросы (keyId), есть ли у него собственный выбор (picked), и целиком тот же ответ, что у GET /keys; без идентификатора — 400. POST { sessionId, keyId } записывает выбор и отвечает тем же payload; keyId: null возвращает чат к ключу по умолчанию

Без перезапусков

Хост-половина перезагружает себя сама, тем же способом, что и dsh-proxy-gui: строка загрузчика называет этот пакет, чья точка входа импортирует реализацию через запрос со сбрасывающим кеш параметром ?rev=, а опрос каталога lib раз в секунду останавливает предыдущую редакцию и запускает следующую, как только изменившийся отпечаток повторится. Поэтому lib/host.js, lib/models.js, lib/keys.js и lib/net.js перезагружаются на лету, а перезапуск нужен только при правке самой точки входа. Браузерная половина приходит в открытую страницу через собственную живую композицию плагинов harness; обновление страницы тоже работает.

Название, описание и значок в меню плагинов читаются из пакета на композиции, поэтому изменение locale/*.json или assets/icon.svg требует перезапуска.

Требования и деградация

Проверено на dsh 0.2.1-rc.2. Пакет не объявляет ни engines.dsh, ни peer-зависимостей, потому что не импортирует ни одного модуля @deepseek-ai/* — у него ноль рантайм-зависимостей, а непроверенное требование к версии хоста дало бы в маркете лишь ложные предупреждения о несовместимости.

Каждая возможность деградирует самостоятельно:

Чего нетЧто происходит
configEditorрасхождение по-прежнему вычисляется и показывается; кнопки записи и отката объясняют, что хост не отдаёт редактор
маршрут dsh-llm-pi-aiзначки работают; половина со списком сообщает, что маршрут не найден
источниккеш на диске отдаёт каждый значок и каждую модель, которые в нём есть
эндпоинт при живом каталогесостав определяет остров, и страница об этом говорит
credentialsзначение ключа некуда записать: кольцо остаётся с одним основным ключом, добавление недоступно, и раздел «API-ключи» называет причину вместо того, чтобы показать форму, которая откажет
configEditor — для кольцазначение существующего ключа заменить можно, маршрут для нового добавить нельзя: форма добавления выключена, и раздел объясняет почему
маршрут ключа, потерянный после ручной правки cordis.patch.yml или отката списка моделейключ остаётся в кольце, его маршрут числится в routes.missing, запросы этого чата идут маршрутом профиля с видимой причиной, а кнопка «Починить маршруты» ({ action: 'repair' }) записывает маршруты кольца заново

Структура

ФайлРоль
lib/index.jsстабильная точка входа загрузчика; владеет циклом перезагрузки
lib/host.jsмаршруты, зеркало каталога, кеш логотипов, синхронизация моделей, кольцо ключей и подмена маршрута на agent/request, настройки
lib/models.jsчистое отображение: что публикует источник → одна запись профиля маршрута, расхождение двух списков и YAML для ручной правки
lib/keys.jsчистая арифметика кольца: названия, имена учётных данных и маршрутов, документы кольца и выбора чатов, сверка карты маршрутов с кольцом
lib/net.jsодин исходящий GET по маршруту процесса, затем по прокси из окружения
client/client.jsбраузерный набор, написанный вручную: декоратор, страница настроек, переключатель ключа в композере и скрытие групп-клонов в меню моделей
locale/en.json, ru.json, zh.jsonназвание и описание для меню плагинов
assets/icon.svgзначок для меню плагинов
cordis.patch.ymlстрока загрузчика, которую подтягивает список bundles профиля
install.ps1установка в профиль
scripts/prepublish-check.mjsгейт публикации: всё, что может установиться и при этом быть молча неправильным
tests/проверки отображения, эмиттера YAML, кольца ключей, хост- и браузерной половин

lib/models.js и lib/keys.js намеренно чистые — без сети, файловой системы и Cordis, — поэтому отображение можно проверить на реальном ответе источника, а арифметику кольца на выдуманном профиле, ничего не монтируя. lib/host.js владеет всем, у чего есть время жизни, и именно его заменяет перезагрузка.

Тесты

node tests\models-check.mjs              # чистое отображение: живой источник -> записи, расхождение, аудит схемы
node tests\yaml-roundtrip-test.mjs       # эмиттер против двух настоящих YAML-парсеров
node tests\model-sync-host-test.mjs      # хост: подставные webServer и configEditor, настоящий запрос к источнику
node tests\api-keys-host-test.mjs        # кольцо ключей: подставные credentials, configEditor и шина событий, без сети
node tests\model-sync-client-test.mjs    # браузер: мини-React и подставной DOM, фиктивный /state
node scripts\prepublish-check.mjs        # гейт публикации

models-check.mjs читает эндпоинт и остров каталога источника, генерирует список и проверяет каждую запись против схемы адаптера: только объявленные поля, положительные целые размеры, известные модальности и словарь рассуждения, который предлагает уровень помимо off, но даёт боевое значение каждому уровню, кроме off. Он же проверяет, что запасной путь по острову выбирает ровно те модели, которые отдаёт эндпоинт, — именно это делает запасной путь безопасным для записи.

yaml-roundtrip-test.mjs разбирает настоящую строку cordis.patch.yml профиля (или синтетическую той же формы, если профиля на машине нет), заново выдаёт её эмиттером этого пакета и проверяет, что два независимых парсера — js-yaml и yaml — читают её обратно одинаково, что все поля подключения сохраняются, что подстановка строки оставляет соседей байт в байт и что расстановка кавычек переживает обратный ход: зарезервированные слова, числа, ведущие индикаторы, концевой пробел и кавычки внутри строки.

Хост-тест прогоняет зарегистрированные маршруты на подставных парах запрос/ответ и подставном configEditor, наполненном маршрутом в том виде, в каком он есть у профиля, и проверяет: план читает эндпоинт; модель, которой зеркало не видело, всё равно получает настоящее отображаемое имя, а не идентификатор; написанный вручную compat переживает перегенерацию и не изобретается для другой модели; /models.yaml выдаёт строку патча, сохраняющую поля подключения и отбрасывающую заменяемый список; запись сохраняет displayName, apiKeyEnv, api и baseURL и заменяет только models; копия ложится на диск; повторное применение ничего не делает и конфигурацию не трогает; /state перестаёт сообщать расхождение, которое запись только что устранила; откат возвращает снятые модели и их compat; нелокальный собеседник получает 403, а неверный метод — 405.

api-keys-host-test.mjs — 84 проверки кольца ключей на той же host-половине, с подставным хранилищем учётных данных, подставным configEditor и подставной шиной событий, без сети вовсе: эндпоинт маршрута смотрит на закрытый порт, поэтому и чтение списка моделей, и проверка ключа детерминированно заканчиваются отказом, а состав списка определяет зеркальный остров. Проверяется подмена: выбранный ключ переименовывает маршрут, а модель, уровень рассуждения и лимит вывода переживают её без изменений; чат без собственного выбора идёт маршрутом профиля; deepseek-account и соседний маршрут с чужим ключом не переименовываются никогда; запрос без сессии остаётся как пришёл; модель, которой клон не отдаёт, оставляет запрос на базовом маршруте и попадает в fallback; чат без выбора следует за ключом по умолчанию, а keyId: null возвращает его обратно. Проверяются отказы: пустой ключ, ключ с пробелом, неизвестное действие (400), удаление основного ключа (409). Проверяется удаление ключа: маршрут клона исчезает, сосед и базовый маршрут выживают, значение стирается из хранилища, выбор чатов с этого ключа сбрасывается, а сам чат продолжает ходить базовым маршрутом. Проверяются записи и счётчики: одно добавление — одна правка конфигурации, замена значения не правит её вовсе, запись списка моделей накрывает клон и сохраняет его собственное имя учётных данных, блок для ручной правки содержит клон, repair возвращает потерянный маршрут, в keys.json нет секрета, выгрузка снимает и маршрут, и слушателя, а с выключенной возможностью не переименовывается ничего.

Браузерный тест рисует страницу настроек мини-реактом, чьи ячейки хуков привязаны к экземпляру компонента, и проверяет: раздел называется Rustvy; три внешних адреса — настоящие ссылки, открывающиеся вне приложения; каждый путь к файлу показан с кнопкой копирования; маршрут, строка профиля, сайт и размер значка редактируются и сохраняются по потере фокуса; оба выпадающих списка сохраняются по изменению; панель ручной правки читает и показывает блок; расхождение называет новые, снятые и изменённые модели; кнопка записи включена при наличии расхождения, выключена при актуальном списке и выключена, когда хост не отдаёт редактор; откат остаётся выключенным, пока копии нет; каждое изменение уходит на нужный маршрут и сообщает, что именно сдвинулось.

Тот же тест рисует и кольцо ключей: раздел «API-ключи» со списком, масками, переименованием по потере фокуса, инлайн-формой «+», проверкой, назначением ключом по умолчанию и удалением в два шага; объяснения на каждый отказ — нет credentials, host-половина устарела, возможность выключена — вместе с возвращением способности, когда причина устранена; переключатель «Ключи на несколько чатов», который читает ответ хоста и пишет своё поле в /config; блок «Маршруты ключей» со счётчиками подмены, числом чатов со своим выбором, путями к обоим файлам, отсутствующим и отставшим маршрутами и кнопкой «Починить маршруты»; предел в 12 ключей; переключатель композера, который не рисуется без sessionId и при одном ключе, читает выбор своего чата, публикует его в /session-key и предлагает добавить ключ; меню выбора моделей и окно /model, которые не получают ни раздела ключей, ни его строк; стоимость обращений к хосту — два взаимодействия подряд стоят одного чтения, открытие переключателя читает всегда, а выбор обходится одним POST без дочитывания; и выгрузка, после которой в меню не остаётся ничего своего. Отдельно проверяется, что значение ключа не попадает ни в один URL и не остаётся на странице после добавления.

Половина, рисующая значки, автоматическим набором в этом репозитории не покрыта — и это стоит знать заранее. Она работает с DOM уже открытой страницы, поэтому проверяется глазами: какие строки получают значок и какой именно, что строки уровней усилия, заголовки производителей и поле поиска остаются пустыми, что вместо неполученного логотипа появляется плашка с монограммой, что добавленная позже строка тоже получает значок, что повторный проход ничего не удваивает и что выгрузка убирает добавленное. model-sync-client-test.mjs покрывает страницу настроек и переключатель ключа в композере, но не рисование значков; скрытие групп-клонов в меню — та же работа с DOM уже открытой страницы, и набор проверяет там только то, что меню не получает ни раздела ключей, ни его строк.

Лицензия

MIT — см. LICENSE.

Comments

Loading…

Similar plugins

dsh-model-router

by andrepontesmelo

DSH plugin: virtual model ids backed by pluggable routing algorithms (priority, round-robin) over real provider candidates, with transparent failover.

Tools & CapabilitiesDevelopment & InfrastructureManifest valid

★ 0

MIT

JavaScript

Oct 12, 2026

dsh plugin --profile web add @andrepontesmelo/dsh-model-router

by Kalospacer

Adds a settings page for declaring per-model reasoning efforts and vision support on pi-ai provider routes.

Models & ProvidersManifest valid

★ 4

MIT

JavaScript

Oct 1, 2026

dsh plugin --profile web add dsh-model-capabilities

by Wany-i

Reorder the model list of any llm-pi-ai route from Settings > Models; the composer model menu follows the same order.

Development & InfrastructureManifest valid

★ 0

MIT

JavaScript

Oct 3, 2026

dsh plugin --profile web add dsh-model-order

by JohnXu22786

Auto-discovers model listings, pricing and capabilities from OpenAI-compatible API hosts, normalizes them, and emits ready-to-use model configs for dsh.

Tools & CapabilitiesManifest valid

★ 1

↓ 5/wk

MIT

TypeScript

Sep 29, 2026

dsh plugin --profile web add model-catalog

by ttmouse

Model picker for the DSH web composer that replaces the model seat: a search-first popup whose left column indexes the suppliers and scrolls the grouped list, with a favorites group pinned at the top,

UI & ExperienceManifest valid

★ 4

↓ 170/wk

JavaScript

Oct 20, 2026

dsh plugin --profile web add dsh-model-picker

by Rczlin

Gives pi-ai routed models a reasoning-effort ladder without manual settings: falls back declared to models.dev to a 7-tier default, and replaces the composer model chip with a Codex-style vertical pic

Models & ProvidersManifest valid

★ 0

JavaScript

Oct 8, 2026

dsh plugin --profile web add @dsh-better-reasoning/plugin