Compare commits
12 Commits
b3468095c9
...
709e144a60
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
709e144a60 | ||
|
|
0502478081 | ||
|
|
41c5cf9b10 | ||
|
|
f4a8c23af3 | ||
|
|
ef187a1b48 | ||
|
|
e6fe58eb37 | ||
|
|
6c507b4549 | ||
|
|
e3a1c500fd | ||
|
|
17a943069a | ||
|
|
5e1e3cc796 | ||
|
|
083a614940 | ||
|
|
08ad4bb429 |
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
Данные правила применяются ко всем агентам, работающим конкретно в этом репозитории.
|
Данные правила применяются ко всем агентам, работающим конкретно в этом репозитории.
|
||||||
|
|
||||||
1. **Полная перегенерация standalone-скриптов при запуске сетап-скрипта (Full regeneration of standalone scripts on setup run):** При каждом запуске `ai-setup.sh` все генерируемые скрипты в выбранном `BIN_DIR` (`~/bin`, если он уже есть в `PATH`, иначе `~/.local/bin`: `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-gemini`, `ai-api-helpers.sh`, `ai-claude`, `claude-gpt-effort-proxy.py`) должны быть **полностью перезаписаны** актуальными версиями. Запрещено выполнять слияние (merge) старого и нового содержимого или дополнение (append). Скрипт обязан привести все генерируемые файлы к эталонному виду, однозначно определяемому текущей конфигурацией.
|
1. **Полная перегенерация standalone-скриптов при запуске сетап-скрипта (Full regeneration of standalone scripts on setup run):** При каждом запуске `ai-setup.sh` все генерируемые скрипты в выбранном `BIN_DIR` (`~/bin`, если он уже есть в `PATH`, иначе `~/.local/bin`: `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-gemini`, `ai-api-helpers.sh`, `ai-claude`, `claude-gpt-effort-proxy.py`) должны быть **полностью перезаписаны** актуальными версиями. Запрещено выполнять слияние (merge) старого и нового содержимого или дополнение (append). Скрипт обязан привести все генерируемые файлы к эталонному виду, однозначно определяемому текущей конфигурацией.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Как это работает
|
## Как это работает
|
||||||
|
|
||||||
`/effort` и `AI_EFFORT` относятся к лаунчерам, которые запускают Claude Code:
|
`/effort` и `AI_EFFORT` относятся к лаунчерам, которые запускают Claude Code:
|
||||||
`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`.
|
`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`.
|
||||||
|
|
||||||
Нативные лаунчеры `ai-gpt` (OpenAI Codex), `ai-gemini` (Antigravity `agy`) и `ai-cursor`
|
Нативные лаунчеры `ai-gpt` (OpenAI Codex), `ai-gemini` (Antigravity `agy`) и `ai-cursor`
|
||||||
(Cursor CLI `agent`) не используют эту схему. У них свои настройки reasoning внутри
|
(Cursor CLI `agent`) не используют эту схему. У них свои настройки reasoning внутри
|
||||||
@@ -15,11 +15,11 @@
|
|||||||
|
|
||||||
| Claude Code `/effort` | Anthropic (`ai-claude`) | DeepSeek V4 (`ai-deepseek`) | Kimi K2.7/K2.6 (`ai-kimi`) | GLM 5.3 (`ai-glm`) | OpenRouter/GPT-5.5 (`ai-openrouter`) | OpenCode Go (`ai-opencode`) |
|
| Claude Code `/effort` | Anthropic (`ai-claude`) | DeepSeek V4 (`ai-deepseek`) | Kimi K2.7/K2.6 (`ai-kimi`) | GLM 5.3 (`ai-glm`) | OpenRouter/GPT-5.5 (`ai-openrouter`) | OpenCode Go (`ai-opencode`) |
|
||||||
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| `low` | нативно `low` | нативно `low` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
|
| `low` | нативно `low` | нативно `low` | thinking on | нативно `low` | без локального маппинга | без локального маппинга |
|
||||||
| `medium` | нативно `medium` | провайдер поднимает до `high` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
|
| `medium` | нативно `medium` | провайдер поднимает до `high` | thinking on | провайдер опускает до `low` | без локального маппинга | без локального маппинга |
|
||||||
| `high` | нативно `high` | нативно `high` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
|
| `high` | нативно `high` | нативно `high` | thinking on | нативно `high` | без локального маппинга | без локального маппинга |
|
||||||
| `xhigh` | нативно `xhigh` | провайдер поднимает до `high` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
|
| `xhigh` | нативно `xhigh` | провайдер поднимает до `high` | thinking on | провайдер опускает до `high` | без локального маппинга | без локального маппинга |
|
||||||
| `max` | нативно `max` | нативно `max` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
|
| `max` | нативно `max` | нативно `max` | thinking on | нативно `max` | без локального маппинга | без локального маппинга |
|
||||||
|
|
||||||
Для OpenRouter текущий лаунчер не делает локального преобразования effort и отправляет
|
Для OpenRouter текущий лаунчер не делает локального преобразования effort и отправляет
|
||||||
значение дальше через Claude Code/OpenRouter. Конкретная интерпретация зависит от выбранной
|
значение дальше через Claude Code/OpenRouter. Конкретная интерпретация зависит от выбранной
|
||||||
@@ -69,12 +69,22 @@ on / off
|
|||||||
### GLM 5.3 (Zhipu AI)
|
### GLM 5.3 (Zhipu AI)
|
||||||
|
|
||||||
```text
|
```text
|
||||||
high / max
|
low -> high -> max
|
||||||
```
|
```
|
||||||
|
|
||||||
- Маппинг выполняется на стороне Zhipu AI API.
|
- 3 реальных уровня, thinking выключить нельзя (`thinking.type=disabled` тоже даёт reasoning).
|
||||||
- Текущий `ai-glm` не делает локального преобразования effort.
|
- Anthropic-endpoint Z.ai читает `output_config.effort` от Claude Code и принимает все 5 уровней
|
||||||
- Дефолт `ai-glm`: `high`.
|
(невалидное значение -> HTTP 400 `reasoning_effort must be one of: none, minimal, low, medium, high, xhigh, max`).
|
||||||
|
- `medium` фактически опускается до `low`, `xhigh` - до `high`.
|
||||||
|
- Если effort не передан, Z.ai использует `max`. Поэтому `ai-glm` выставляет
|
||||||
|
`CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1` - иначе Claude Code не считает `glm-5.3` effort-capable
|
||||||
|
и параметр не уходит.
|
||||||
|
- Маппинг выполняется на стороне Z.ai API, `ai-glm` локально effort не преобразует.
|
||||||
|
- Дефолт `ai-glm`: `xhigh` (нативный `high` у Z.ai), задаётся через CLI `--effort` при каждом запуске.
|
||||||
|
- Официально Z.ai маппинг `medium`/`xhigh` не документирует. Он выведен замером 2026-09-25:
|
||||||
|
по 3 запроса на уровень, средний `output_tokens` на счётной задаче - `low` ~1.5K,
|
||||||
|
`medium` ~1.1K, `high` ~3.4K, `xhigh` ~2.8K, `max` ~3.8K. `low`/`medium` уверенно короче
|
||||||
|
остальных, а `high` и `max` на такой выборке пересекаются по разбросу.
|
||||||
|
|
||||||
### OpenRouter / GPT-5.5
|
### OpenRouter / GPT-5.5
|
||||||
|
|
||||||
@@ -89,6 +99,51 @@ high / max
|
|||||||
Effort не преобразуется локально — thinking budget не имеет прямого эффекта на OpenCode Go бекенд.
|
Effort не преобразуется локально — thinking budget не имеет прямого эффекта на OpenCode Go бекенд.
|
||||||
Дефолт `ai-opencode`: `high`.
|
Дефолт `ai-opencode`: `high`.
|
||||||
|
|
||||||
|
### Xiaomi MiMo V2.6
|
||||||
|
|
||||||
|
```text
|
||||||
|
thinking on (единственный уровень)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `ai-mimo` работает через Claude Code с Anthropic-эндпоинтом Xiaomi (Token Plan региона
|
||||||
|
подписки или `api.xiaomimimo.com`).
|
||||||
|
- Claude Code 2.1.294 отправляет `thinking: {type: "adaptive"}` и `output_config.effort` со всеми
|
||||||
|
5 уровнями и для `mimo-v2.6-pro`, и для `mimo-v2.6-flash`, даже без
|
||||||
|
`CLAUDE_CODE_ALWAYS_ENABLE_EFFORT` (проверено перехватом запроса).
|
||||||
|
- MiMo effort игнорирует. В доке Anthropic API MiMo из параметров рассуждения есть только
|
||||||
|
`thinking.type` (`enabled` по умолчанию / `disabled` / `adaptive`), `effort` и `budget_tokens` нет.
|
||||||
|
Невалидные `output_config.effort=bogus` и `thinking.type=bogus` дают HTTP 200.
|
||||||
|
- Замер 2026-10-13, по 3 запроса на уровень, средний `output_tokens`. Pro, счётная задача:
|
||||||
|
без effort ~310, `low` ~340, `high` ~280, `max` ~315. Pro, задача на включения-исключения:
|
||||||
|
`low` ~970, `max` ~825. Flash на ней же: `low` ~460, `max` ~580. Разброс внутри уровня больше
|
||||||
|
разницы между уровнями, тренда нет.
|
||||||
|
- Реально переключается только thinking: `thinking.type=disabled` даёт 4 токена без размышлений
|
||||||
|
и неверные ответы (2 из 3). Claude Code его для MiMo не отправляет.
|
||||||
|
- `thinking.type=adaptive` (его шлёт Claude Code) MiMo отрабатывает так же, как `enabled`:
|
||||||
|
размышления есть даже на «скажи привет», на задаче на включения-исключения `adaptive` ~670,
|
||||||
|
`enabled` ~600 токенов при разбросе 356-1161. `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1`
|
||||||
|
в Claude Code 2.1.294 запрос не меняет, да и не нужен.
|
||||||
|
- Глубину размышлений ограничивает только `max_tokens`: `ai-mimo` выставляет
|
||||||
|
`CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000` (потолок Claude Code, MiMo принимает до 131072).
|
||||||
|
- Дефолт `ai-mimo`: `max`, задаётся через CLI `--effort` при каждом запуске (как у `ai-eltex-qwen`).
|
||||||
|
На размышления не влияет, но если Xiaomi начнёт учитывать effort, лаунчер сразу будет на максимуме.
|
||||||
|
|
||||||
|
### Qwen3.8 (Alibaba Token Plan)
|
||||||
|
|
||||||
|
```text
|
||||||
|
low -> medium -> high -> xhigh -> max (принимаются, у Max тренда нет)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `ai-qwen` работает через Claude Code с Anthropic-эндпоинтом Alibaba Model Studio
|
||||||
|
(`token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic`).
|
||||||
|
- Qwen проверяет `output_config.effort`: `bogus` даёт HTTP 400 со списком
|
||||||
|
`low, medium, high, xhigh, max`. `thinking.type=adaptive` принимается, размышления есть всегда.
|
||||||
|
- Замер 2026-10-20, задача на включения-исключения, `output_tokens`. `qwen3.8-max`, по 3 запроса:
|
||||||
|
`low` 435/513/615, `high` 363/412/662, `max` 397/539/657 - разброс больше разницы, тренда нет.
|
||||||
|
`qwen3.8-flash`, по 2 запроса: `low` ~430, `max` ~790.
|
||||||
|
- Дефолт `ai-qwen`: `max` через CLI `--effort` при каждом запуске, `CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1`
|
||||||
|
как страховка.
|
||||||
|
|
||||||
`claude-gpt-effort-proxy.py` все еще генерируется как совместимый helper для старого
|
`claude-gpt-effort-proxy.py` все еще генерируется как совместимый helper для старого
|
||||||
`claude-code-proxy` backend-а. Если использовать его вручную, он маппит только
|
`claude-code-proxy` backend-а. Если использовать его вручную, он маппит только
|
||||||
`xhigh` -> `high`, потому что `claude-code-proxy` принимает `low`, `medium`, `high`, `max`.
|
`xhigh` -> `high`, потому что `claude-code-proxy` принимает `low`, `medium`, `high`, `max`.
|
||||||
@@ -100,14 +155,16 @@ Effort не преобразуется локально — thinking budget не
|
|||||||
| Anthropic | Не нужен | - |
|
| Anthropic | Не нужен | - |
|
||||||
| DeepSeek V4 | DeepSeek API | На стороне сервера |
|
| DeepSeek V4 | DeepSeek API | На стороне сервера |
|
||||||
| Kimi K2.7/K2.6 | Moonshot API | На стороне сервера |
|
| Kimi K2.7/K2.6 | Moonshot API | На стороне сервера |
|
||||||
| GLM 5.3 | Zhipu AI API | На стороне сервера |
|
| GLM 5.3 | Z.ai API (`low,medium→low`, `high,xhigh→high`, `max`) | На стороне сервера |
|
||||||
|
| Xiaomi MiMo V2.6 | Не маппится: MiMo принимает `output_config.effort`, но игнорирует его | - |
|
||||||
|
| Qwen3.8 | Alibaba API, значение уходит как есть | На стороне сервера |
|
||||||
| OpenRouter/GPT-5.5 | Локального маппинга нет | `ai-openrouter` отправляет значение как есть |
|
| OpenRouter/GPT-5.5 | Локального маппинга нет | `ai-openrouter` отправляет значение как есть |
|
||||||
| OpenCode Go | Локального маппинга нет | `ai-opencode` отправляет значение через routatic-proxy |
|
| OpenCode Go | Локального маппинга нет | `ai-opencode` отправляет значение через routatic-proxy |
|
||||||
| Legacy GPT proxy | helper-маппинг `xhigh` -> `high` | `~/bin/claude-gpt-effort-proxy.py` или `~/.local/bin/claude-gpt-effort-proxy.py` |
|
| Legacy GPT proxy | helper-маппинг `xhigh` -> `high` | `~/bin/claude-gpt-effort-proxy.py` или `~/.local/bin/claude-gpt-effort-proxy.py` |
|
||||||
|
|
||||||
## Persistence effort между сессиями
|
## Persistence effort между сессиями
|
||||||
|
|
||||||
Каждый Claude Code лаунчер (`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`)
|
Каждый Claude Code лаунчер (`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`)
|
||||||
запоминает свой уровень effort отдельно в `settings.json` лаунчера.
|
запоминает свой уровень effort отдельно в `settings.json` лаунчера.
|
||||||
|
|
||||||
`/effort` внутри сессии работает для всех уровней. `CLAUDE_CODE_EFFORT_LEVEL` не используется.
|
`/effort` внутри сессии работает для всех уровней. `CLAUDE_CODE_EFFORT_LEVEL` не используется.
|
||||||
@@ -120,10 +177,17 @@ AI_EFFORT=high ai-deepseek # включить high
|
|||||||
ai-deepseek # без флага - использовать уровень из settings.json
|
ai-deepseek # без флага - использовать уровень из settings.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Дефолты: `xhigh` для `ai-claude`, `high` для остальных Claude Code лаунчеров.
|
Дефолты: `xhigh` для `ai-claude` и `ai-glm`, `max` для `ai-eltex`, `ai-eltex-qwen`, `ai-mimo` и `ai-qwen`, `high` для
|
||||||
|
остальных Claude Code лаунчеров. `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo` и `ai-qwen` передают уровень через CLI
|
||||||
|
`--effort`, поэтому каждый запуск стартует на дефолте (или на `AI_EFFORT`), а `/effort` внутри сессии
|
||||||
|
действует до её конца.
|
||||||
|
|
||||||
## Рекомендации
|
## Рекомендации
|
||||||
|
|
||||||
- Для повседневной работы: `high` или `xhigh`.
|
- Для повседневной работы: `high` или `xhigh`.
|
||||||
- `max` effort имеет реальный эффект у Anthropic и DeepSeek; у Kimi и GLM это всё тот же thinking on / маппинг на стороне сервера.
|
- `max` effort имеет реальный эффект у Anthropic, DeepSeek и GLM; у Kimi это всё тот же thinking on.
|
||||||
- `low`/`medium` у Kimi и GLM фактически не снижают reasoning; у DeepSeek `low` теперь даёт реальный уровень `low` и экономит бюджет.
|
- `low`/`medium` у Kimi фактически не снижают reasoning; у DeepSeek `low` и у GLM `low`/`medium` дают реальный уровень `low` и экономят бюджет.
|
||||||
|
- У GLM осмысленно выбирать только `low`, `high` (или `xhigh`) и `max`.
|
||||||
|
- `ai-eltex` (путь прокси `/anthropic`, 20.10): запрос идёт на Anthropic-эндпоинт Z.ai как есть,
|
||||||
|
effort у `glm-5.3` работает: `low` - без размышлений, дальше `medium` < `high` < `max`. Как и `ai-glm`, стартует на `max`
|
||||||
|
через CLI `--effort`, `AI_EFFORT=high ai-eltex` работает. У `ai-eltex-qwen` прокси effort игнорирует, `/effort` и `AI_EFFORT` там ни на что не влияют.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ bash setup.sh
|
|||||||
```
|
```
|
||||||
|
|
||||||
Установит лаунчеры
|
Установит лаунчеры
|
||||||
`ai-claude`, `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`, `ai-gemini`, `ai-cursor`
|
`ai-claude`, `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`, `ai-gemini`, `ai-cursor`
|
||||||
в `~/bin`, если он уже есть в `PATH`, иначе в `~/.local/bin/`, и запишет API-ключи.
|
в `~/bin`, если он уже есть в `PATH`, иначе в `~/.local/bin/`, и запишет API-ключи.
|
||||||
|
|
||||||
После установки, если команды не видны:
|
После установки, если команды не видны:
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -113,7 +113,7 @@ sudo GATEWAY=10.0.0.1 DEV=eth0 bash scripts/ru-bypass.sh
|
|||||||
- `ru-bypass.timer` - ежедневно запускает обновление RIPE-списка и маршрутов
|
- `ru-bypass.timer` - ежедневно запускает обновление RIPE-списка и маршрутов
|
||||||
- NM dispatcher `/etc/NetworkManager/dispatcher.d/99-ru-bypass` - перезапускает скрипт при поднятии amn0
|
- NM dispatcher `/etc/NetworkManager/dispatcher.d/99-ru-bypass` - перезапускает скрипт при поднятии amn0
|
||||||
- `/etc/ru-bypass.conf` - root-конфиг для запусков из systemd/NM dispatcher
|
- `/etc/ru-bypass.conf` - root-конфиг для запусков из systemd/NM dispatcher
|
||||||
- `/etc/hosts` блок для локальных Eltex-доменов
|
- `/etc/hosts` блок для локальных Eltex-доменов (между `# ru-bypass: local hosts` и `# ru-bypass: end`; ручные записи вне блока скрипт не трогает)
|
||||||
|
|
||||||
При каждом запуске:
|
При каждом запуске:
|
||||||
|
|
||||||
@@ -161,6 +161,10 @@ bash tests/test_network.sh # полные тесты
|
|||||||
- `ai-deepseek` - Claude Code через DeepSeek API
|
- `ai-deepseek` - Claude Code через DeepSeek API
|
||||||
- `ai-kimi` - Claude Code через официальный Kimi Code API (`api.kimi.com/coding`)
|
- `ai-kimi` - Claude Code через официальный Kimi Code API (`api.kimi.com/coding`)
|
||||||
- `ai-glm` - Claude Code через Zhipu AI GLM 5.3 API (`api.z.ai/api/anthropic`)
|
- `ai-glm` - Claude Code через Zhipu AI GLM 5.3 API (`api.z.ai/api/anthropic`)
|
||||||
|
- `ai-eltex` - Claude Code через корпоративный LLM-прокси Eltex (`http://llm.eltex.loc:4000`, LiteLLM, team-подписка GLM). Ключ выдают админы ([дока](https://intdocs.eltex.loc/pages/viewpage.action?pageId=262329878)), Claude Code ходит в сырой путь прокси `/anthropic`, который пересылает запросы на Anthropic-эндпоинт Z.ai как есть; модель `glm-5.3` во всех слотах `/model` (картинки через серверный `analyze_image`, как в `ai-glm`, effort, окно 1M); если ключу недоступна `glm-5.3`, лаунчер выходит с понятной ошибкой; переопределение: `ELTEX_MODEL`, `ELTEX_BASE_URL`
|
||||||
|
- `ai-eltex-qwen` - то же через корпоративный LLM-прокси Eltex, но на моделях Qwen со своим ключом от админов: по умолчанию `Eltex-Coder-Senior` (Qwen3.8-27B), если она есть у ключа, иначе `Eltex-Qwen3_8-Flash-Next`; ключ можно задать в `AI_ELTEX_LLM_API_KEY`, как в онбординге Eltex AI; переопределение: `ELTEX_QWEN_MODEL`, `ELTEX_BASE_URL`
|
||||||
|
- `ai-mimo` - Claude Code через Xiaomi MiMo V2.6 (Anthropic-совместимый API, Token Plan или pay-as-you-go): всегда самая умная модель `mimo-v2.6-pro` (во всех слотах и сабагентах, `--model opus` на каждом запуске), effort `max`, вывод до 128000 токенов, окно 1M. Эндпоинт региона подписки (`token-plan-sgp`/`-ams`, для `sk-` ключа `api.xiaomimimo.com`) находится перебором при вводе ключа; переопределение: `MIMO_BASE_URL` (нужно для `token-plan-cn`: он в перебор не входит, с наших машин не отвечает)
|
||||||
|
- `ai-qwen` - Claude Code через Alibaba Cloud Model Studio (Token Plan, Anthropic-совместимый API `token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic`): общий ключ вшит в скрипт, запускается сразу без ввода ключа. `qwen3.8-max` в opus/sonnet и сабагентах (`--model opus` на каждом запуске), `qwen3.8-flash` в haiku, effort `max`, окно 1M, картинки видят обе модели
|
||||||
- `ai-openrouter` - Claude Code через OpenRouter (GPT-5.5, Grok, Qwen, GLM 5.2 и др.)
|
- `ai-openrouter` - Claude Code через OpenRouter (GPT-5.5, Grok, Qwen, GLM 5.2 и др.)
|
||||||
- `ai-opencode` - Claude Code через OpenCode Go + routatic-proxy (DeepSeek, Kimi, Qwen, GLM, MiniMax, MiMo)
|
- `ai-opencode` - Claude Code через OpenCode Go + routatic-proxy (DeepSeek, Kimi, Qwen, GLM, MiniMax, MiMo)
|
||||||
- `ai-gemini` - нативный Antigravity CLI `agy`
|
- `ai-gemini` - нативный Antigravity CLI `agy`
|
||||||
@@ -218,6 +222,9 @@ android init
|
|||||||
- `~/.config/ai-setup/deepseek_key` - DeepSeek API key (права 600)
|
- `~/.config/ai-setup/deepseek_key` - DeepSeek API key (права 600)
|
||||||
- `~/.config/ai-setup/kimi_key` - Kimi API key (права 600)
|
- `~/.config/ai-setup/kimi_key` - Kimi API key (права 600)
|
||||||
- `~/.config/ai-setup/glm_key` - Zhipu AI (GLM) API key (права 600)
|
- `~/.config/ai-setup/glm_key` - Zhipu AI (GLM) API key (права 600)
|
||||||
|
- `~/.config/ai-setup/eltex_key` - ключ корпоративного LLM-прокси Eltex от админов (права 600)
|
||||||
|
- `~/.config/ai-setup/eltex_qwen_key` - ключ для моделей Qwen на том же прокси (права 600)
|
||||||
|
- `~/.config/ai-setup/mimo_key` - Xiaomi MiMo API key (права 600), `mimo_base_url` - найденный для него эндпоинт
|
||||||
- `~/.config/ai-setup/openrouter_key` - OpenRouter API key (права 600)
|
- `~/.config/ai-setup/openrouter_key` - OpenRouter API key (права 600)
|
||||||
- `~/.config/ai-setup/opencode_key` - OpenCode Go API key (права 600)
|
- `~/.config/ai-setup/opencode_key` - OpenCode Go API key (права 600)
|
||||||
- `~/.config/ai-setup/global_rules.md` - глобальные правила агентов
|
- `~/.config/ai-setup/global_rules.md` - глобальные правила агентов
|
||||||
@@ -226,7 +233,7 @@ android init
|
|||||||
## Права запуска агентов
|
## Права запуска агентов
|
||||||
|
|
||||||
- `ai-gpt` использует `--dangerously-bypass-approvals-and-sandbox`
|
- `ai-gpt` использует `--dangerously-bypass-approvals-and-sandbox`
|
||||||
- `ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`, `ai-gemini` используют `--dangerously-skip-permissions`
|
- `ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`, `ai-gemini` используют `--dangerously-skip-permissions`
|
||||||
- `ai-cursor` использует `--force` (алиас `--yolo`)
|
- `ai-cursor` использует `--force` (алиас `--yolo`)
|
||||||
|
|
||||||
Удобно для локального coding workflow, но это не sandbox для недоверенного кода.
|
Удобно для локального coding workflow, но это не sandbox для недоверенного кода.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Copyright (c) 2026 Виталий Никитенко. MIT License.
|
# Copyright (c) 2026 Виталий Никитенко. MIT License.
|
||||||
# UserPromptSubmit hook: перехватывает /logout, /logout-glm, /logout-deepseek, /logout-kimi.
|
# UserPromptSubmit hook: перехватывает /logout, /logout-glm, /logout-eltex, /logout-eltex-qwen, /logout-mimo, /logout-deepseek, /logout-kimi.
|
||||||
# Удаляет сохранённый API-ключ провайдера, чтобы при следующем запуске лаунчер
|
# Удаляет сохранённый API-ключ провайдера, чтобы при следующем запуске лаунчер
|
||||||
# запросил новый.
|
# запросил новый.
|
||||||
# /logout-glm | /logout-deepseek | /logout-kimi — явно указанный провайдер.
|
# /logout-glm | /logout-eltex | /logout-eltex-qwen | /logout-mimo | /logout-deepseek | /logout-kimi — явно указанный провайдер.
|
||||||
# /logout (без суффикса) — провайдер по AI_LAUNCHER (glm/deepseek/kimi).
|
# /logout (без суффикса) — провайдер по AI_LAUNCHER (glm/eltex/eltex-qwen/mimo/deepseek/kimi).
|
||||||
# Для ai-claude и прочих лаунчеров /logout отступает (exit 0) — пусть работает
|
# Для ai-claude и прочих лаунчеров /logout отступает (exit 0) — пусть работает
|
||||||
# нативный oauth-logout. exit 2 после удаления: LLM не нужна, и нативная
|
# нативный oauth-logout. exit 2 после удаления: LLM не нужна, и нативная
|
||||||
# /logout не пытается сделать бессмысленный oauth-logout для api-key провайдеров.
|
# /logout не пытается сделать бессмысленный oauth-logout для api-key провайдеров.
|
||||||
@@ -17,11 +17,14 @@ cmd=$(echo "$normalized" | awk '{print $1}')
|
|||||||
target=""
|
target=""
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
logout-glm) target=glm ;;
|
logout-glm) target=glm ;;
|
||||||
|
logout-eltex) target=eltex ;;
|
||||||
|
logout-eltex-qwen) target=eltex-qwen ;;
|
||||||
|
logout-mimo) target=mimo ;;
|
||||||
logout-deepseek) target=deepseek ;;
|
logout-deepseek) target=deepseek ;;
|
||||||
logout-kimi) target=kimi ;;
|
logout-kimi) target=kimi ;;
|
||||||
logout)
|
logout)
|
||||||
case "${AI_LAUNCHER:-claude}" in
|
case "${AI_LAUNCHER:-claude}" in
|
||||||
glm|deepseek|kimi) target="$AI_LAUNCHER" ;;
|
glm|eltex|eltex-qwen|mimo|deepseek|kimi) target="$AI_LAUNCHER" ;;
|
||||||
*) exit 0 ;;
|
*) exit 0 ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
@@ -35,6 +38,18 @@ case "$target" in
|
|||||||
rm -f "$CFG/glm_key"
|
rm -f "$CFG/glm_key"
|
||||||
echo "✓ GLM ключ удалён. Перезапусти ai-glm, чтобы ввести новый." >&2
|
echo "✓ GLM ключ удалён. Перезапусти ai-glm, чтобы ввести новый." >&2
|
||||||
;;
|
;;
|
||||||
|
eltex)
|
||||||
|
rm -f "$CFG/eltex_key"
|
||||||
|
echo "✓ Eltex ключ удалён. Перезапусти ai-eltex, чтобы ввести новый." >&2
|
||||||
|
;;
|
||||||
|
eltex-qwen)
|
||||||
|
rm -f "$CFG/eltex_qwen_key"
|
||||||
|
echo "✓ Eltex Qwen ключ удалён. Перезапусти ai-eltex-qwen, чтобы ввести новый." >&2
|
||||||
|
;;
|
||||||
|
mimo)
|
||||||
|
rm -f "$CFG/mimo_key" "$CFG/mimo_base_url"
|
||||||
|
echo "✓ MiMo ключ удалён. Перезапусти ai-mimo, чтобы ввести новый." >&2
|
||||||
|
;;
|
||||||
deepseek)
|
deepseek)
|
||||||
rm -f "$CFG/deepseek_key"
|
rm -f "$CFG/deepseek_key"
|
||||||
echo "✓ DeepSeek ключ удалён. Перезапусти ai-deepseek, чтобы ввести новый." >&2
|
echo "✓ DeepSeek ключ удалён. Перезапусти ai-deepseek, чтобы ввести новый." >&2
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ _brand_color() {
|
|||||||
claude) printf '\033[38;5;173m' ;; # Anthropic оранжевый
|
claude) printf '\033[38;5;173m' ;; # Anthropic оранжевый
|
||||||
kimi) printf '\033[38;5;81m' ;; # Kimi голубой
|
kimi) printf '\033[38;5;81m' ;; # Kimi голубой
|
||||||
glm) printf '\033[38;5;223m' ;; # Z.ai кремовый (бренд-фон z.ai, #FFE4B5)
|
glm) printf '\033[38;5;223m' ;; # Z.ai кремовый (бренд-фон z.ai, #FFE4B5)
|
||||||
|
mimo) printf '\033[38;5;202m' ;; # Xiaomi оранжевый (#FF6900)
|
||||||
|
qwen) printf '\033[38;5;99m' ;; # Qwen фиолетовый (#615CED)
|
||||||
openrouter) printf '\033[38;5;135m' ;; # OpenRouter фиолетовый
|
openrouter) printf '\033[38;5;135m' ;; # OpenRouter фиолетовый
|
||||||
*) printf '\033[38;5;223m' ;; # кремовый (fallback)
|
*) printf '\033[38;5;223m' ;; # кремовый (fallback)
|
||||||
esac
|
esac
|
||||||
@@ -268,6 +270,24 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Z.ai (ai-glm, ai-eltex) отдаёт usage только в конце ответа, в начале потока
|
||||||
|
# input_tokens=0, поэтому пока идёт запрос, Claude Code присылает used_percentage=0.
|
||||||
|
# Держим последнее ненулевое значение этой сессии.
|
||||||
|
session_id=$(echo "$input" | jq -r '.session_id // empty')
|
||||||
|
if [ -n "$session_id" ]; then
|
||||||
|
ctx_cache="$HOME/.cache/ai-setup/ctx_last_${session_id}"
|
||||||
|
if [ -n "$ctx_pct" ] && [ "$(printf '%.0f' "$ctx_pct")" -gt 0 ]; then
|
||||||
|
if [ ! -f "$ctx_cache" ]; then
|
||||||
|
mkdir -p "$HOME/.cache/ai-setup"
|
||||||
|
# Раз за сессию чистим кэш старых сессий
|
||||||
|
find "$HOME/.cache/ai-setup" -name 'ctx_last_*' -mtime +7 -delete 2>/dev/null
|
||||||
|
fi
|
||||||
|
echo "$ctx_pct" > "$ctx_cache"
|
||||||
|
elif [ -f "$ctx_cache" ]; then
|
||||||
|
ctx_pct=$(cat "$ctx_cache")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# ctx:0% при старте новой сессии (нет данных от API)
|
# ctx:0% при старте новой сессии (нет данных от API)
|
||||||
[ -z "$ctx_pct" ] && ctx_pct="0"
|
[ -z "$ctx_pct" ] && ctx_pct="0"
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ before.rules: 10/8, 172.16/12,
|
|||||||
5. Добавляет маршруты для RFC1918 диапазонов (`10/8`, `172.16/12`, `192.168/16`) - нужно для `*.loc`
|
5. Добавляет маршруты для RFC1918 диапазонов (`10/8`, `172.16/12`, `192.168/16`) - нужно для `*.loc`
|
||||||
6. Добавляет встроенные исключения kill switch (`mattermost.eltex-co.ru`, `elph.eltex-co.ru`, `10.80.0.15`)
|
6. Добавляет встроенные исключения kill switch (`mattermost.eltex-co.ru`, `elph.eltex-co.ru`, `10.80.0.15`)
|
||||||
7. Добавляет пользовательские исключения из `AMNEZIA_SERVER` и `KILL_SWITCH_EXCEPTIONS`
|
7. Добавляет пользовательские исключения из `AMNEZIA_SERVER` и `KILL_SWITCH_EXCEPTIONS`
|
||||||
8. Обновляет блок локальных Eltex-хостов в `/etc/hosts`
|
8. Обновляет блок локальных Eltex-хостов в `/etc/hosts` (только между маркерами `# ru-bypass: local hosts` / `# ru-bypass: end`, ручные записи сохраняются)
|
||||||
9. Если задан `LOCAL_DNS`, настраивает `resolvectl` для `~loc` на интерфейсе `DEV`
|
9. Если задан `LOCAL_DNS`, настраивает `resolvectl` для `~loc` на интерфейсе `DEV`
|
||||||
10. Обновляет `/etc/ufw/before.rules`, прямые iptables-правила и отключает IPv6 при активном kill switch
|
10. Обновляет `/etc/ufw/before.rules`, прямые iptables-правила и отключает IPv6 при активном kill switch
|
||||||
11. При первом запуске: устанавливает systemd service/timer и NetworkManager dispatcher
|
11. При первом запуске: устанавливает systemd service/timer и NetworkManager dispatcher
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Copyright (c) 2026 Виталий Никитенко. MIT License.
|
# Copyright (c) 2026 Виталий Никитенко. MIT License.
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# AI Setup - Claude / GPT-5.5 / Grok / Qwen / DeepSeek / Kimi / GLM / Gemini
|
# AI Setup - Claude / GPT-5.5 / Grok / Qwen / DeepSeek / Kimi / GLM / Eltex / Eltex Qwen / Gemini
|
||||||
# Запуск: bash ai-setup.sh
|
# Запуск: bash ai-setup.sh
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
@@ -1649,12 +1649,18 @@ export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/glm"
|
|||||||
# Claude Code навязывает 3 слота; незаданный показал бы чужой Claude, поэтому
|
# Claude Code навязывает 3 слота; незаданный показал бы чужой Claude, поэтому
|
||||||
# opus+sonnet+haiku = glm-5.3. Окно контекста у всех слотов задаёт env
|
# opus+sonnet+haiku = glm-5.3. Окно контекста у всех слотов задаёт env
|
||||||
# CLAUDE_CODE_MAX_CONTEXT_TOKENS ниже — суффикс [1m] больше не нужен.
|
# CLAUDE_CODE_MAX_CONTEXT_TOKENS ниже — суффикс [1m] больше не нужен.
|
||||||
_setup_isolated_config glm opus high ''
|
_setup_isolated_config glm opus xhigh ''
|
||||||
# ai-glm стартует на /effort max. max невалиден в settings.json (там только
|
# ai-glm стартует на /effort xhigh через CLI --effort (перебивает effortLevel из
|
||||||
# low/medium/high/xhigh), поэтому задаём через CLI --effort, а не _apply_effort.
|
# settings.json при каждом запуске). Z.ai сворачивает 5 уровней Claude Code в 3
|
||||||
|
# нативных GLM 5.3: low,medium→low; high,xhigh→high; max→max (см. EFFORT_MAPPING.md),
|
||||||
|
# то есть xhigh = нативный high.
|
||||||
# CLAUDE_CODE_ALWAYS_ENABLE_EFFORT заставляет Claude Code отправлять effort в API
|
# CLAUDE_CODE_ALWAYS_ENABLE_EFFORT заставляет Claude Code отправлять effort в API
|
||||||
# для GLM (иначе glm-5.3 не распознан как effort-capable и параметр не уходит).
|
# для GLM (иначе glm-5.3 не распознан как effort-capable и параметр не уходит,
|
||||||
# /effort внутри сессии работает; ручной override запуска: AI_EFFORT=high ai-glm.
|
# а Z.ai по умолчанию берёт max).
|
||||||
|
# /effort внутри сессии работает; ручной override запуска: AI_EFFORT=max ai-glm.
|
||||||
|
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000: по умолчанию Claude Code просит у glm-5.3
|
||||||
|
# max_tokens=32000 (размышления + ответ), на трудных задачах GLM в них не укладывается
|
||||||
|
# и ответ обрывается. Z.ai принимает и 128000.
|
||||||
# CLAUDE_CODE_DISABLE_1M_CONTEXT НЕ выставляем: GLM 5.3 реально держит 1M контекста,
|
# CLAUDE_CODE_DISABLE_1M_CONTEXT НЕ выставляем: GLM 5.3 реально держит 1M контекста,
|
||||||
# а флаг заставлял бы счётчик ctx: считать окно = 200K (врал в ~5 раз). Прочие
|
# а флаг заставлял бы счётчик ctx: считать окно = 200K (врал в ~5 раз). Прочие
|
||||||
# лаунчеры (deepseek/kimi/openrouter/opencode) флаг оставляют — у них другой контекст
|
# лаунчеры (deepseek/kimi/openrouter/opencode) флаг оставляют — у них другой контекст
|
||||||
@@ -1676,12 +1682,583 @@ ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="GLM 5.3" \
|
|||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="GLM 5.3 — флагман Zhipu AI" \
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="GLM 5.3 — флагман Zhipu AI" \
|
||||||
CLAUDE_CODE_SUBAGENT_MODEL=glm-5.3 \
|
CLAUDE_CODE_SUBAGENT_MODEL=glm-5.3 \
|
||||||
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
|
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
|
||||||
|
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
|
||||||
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
|
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
|
||||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||||
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-xhigh}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||||
GLMEOF
|
GLMEOF
|
||||||
chmod +x "$BIN_DIR/ai-glm"
|
chmod +x "$BIN_DIR/ai-glm"
|
||||||
|
|
||||||
|
# === ai-eltex ===
|
||||||
|
cat > "$BIN_DIR/ai-eltex" << 'ELTEXEOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ai-eltex - запуск Claude Code через корпоративный LLM-прокси Eltex (LiteLLM, team-подписка GLM).
|
||||||
|
# Персональный ключ выдают админы. Дока:
|
||||||
|
# https://intdocs.eltex.loc/pages/viewpage.action?pageId=262329878
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
key_file="$HOME/.config/ai-setup/eltex_key"
|
||||||
|
api_key=""
|
||||||
|
reauth=0
|
||||||
|
|
||||||
|
# Принудительный сброс сохранённого ключа через AI_RESET_KEY=1
|
||||||
|
if [ "${AI_RESET_KEY:-0}" == "1" ] && [ -f "$key_file" ]; then
|
||||||
|
rm -f "$key_file"
|
||||||
|
api_key=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$key_file" ] && api_key=$(cat "$key_file")
|
||||||
|
|
||||||
|
eltex_base_url="${ELTEX_BASE_URL:-http://llm.eltex.loc:4000}"
|
||||||
|
|
||||||
|
# _eltex_check_key: проверка ключа через GET /v1/models. Виртуальному ключу LiteLLM
|
||||||
|
# доступны только LLM-роуты, а /v1/models заодно отдаёт модели, разрешённые ключу.
|
||||||
|
_eltex_check_key() {
|
||||||
|
local response
|
||||||
|
# Без "|| echo 000": при сетевой ошибке -w сам печатает 000.
|
||||||
|
response=$(curl -s -w "\n%{http_code}" --max-time 15 "$eltex_base_url/v1/models" \
|
||||||
|
-H "Authorization: Bearer $1" \
|
||||||
|
2>/dev/null)
|
||||||
|
_CLAUDE_TEST_CODE=$(echo "$response" | tail -1)
|
||||||
|
_CLAUDE_TEST_BODY=$(echo "$response" | sed '$d')
|
||||||
|
if [ "$_CLAUDE_TEST_CODE" = "000" ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[0;31m[СЕТЬ]\033[0m Прокси $eltex_base_url недоступен."
|
||||||
|
echo " Нужна сеть Eltex. Если не резолвится llm.eltex.loc - перезапусти ru-bypass (добавит его в /etc/hosts)"
|
||||||
|
echo " или задай адрес явно: ELTEX_BASE_URL=http://172.16.4.68:4000 ai-eltex"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
_handle_openai_api_response "Eltex LLM" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Лимит team-подписки исчерпан - обратись к админам"
|
||||||
|
}
|
||||||
|
|
||||||
|
# _eltex_pick_models: модель glm-5.3 из _CLAUDE_TEST_BODY (последний ответ /v1/models).
|
||||||
|
# Если имя с суффиксом (glm-5.3-2), берём точное имя, иначе первое с тем же префиксом, кроме
|
||||||
|
# flash; имена групп доступа (ZAI-Test-Group) так не попадут. Список не распарсился
|
||||||
|
# (429/сетевая ошибка/5xx) - стандартное имя наугад. Распарсился, но glm-5.3 нет - ошибка
|
||||||
|
# в stderr, model пустой, возврат 1: дефолтный запуск невозможен.
|
||||||
|
_eltex_pick_models() {
|
||||||
|
local picked avail
|
||||||
|
if ! picked=$(echo "$_CLAUDE_TEST_BODY" | python3 -c '
|
||||||
|
import sys, json
|
||||||
|
ids = [m["id"] for m in json.load(sys.stdin)["data"]]
|
||||||
|
def pick(exact, prefix, skip=None):
|
||||||
|
if exact in ids:
|
||||||
|
return exact
|
||||||
|
return next((i for i in ids if i.startswith(prefix) and not (skip and skip in i)), "-")
|
||||||
|
print(pick("glm-5.3", "glm-5.3-", "flash"), " ".join(ids) or "(пусто)")' 2>/dev/null); then
|
||||||
|
model="glm-5.3"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
read -r model avail <<< "$picked"
|
||||||
|
if [ "$model" = "-" ]; then
|
||||||
|
model=""
|
||||||
|
echo -e "\033[0;31m[МОДЕЛЬ]\033[0m Ключу недоступна модель glm-5.3. Прокси отдаёт: $avail" >&2
|
||||||
|
echo " Напиши админам про доступ к glm-5.3 или задай модель явно: ELTEX_MODEL=<имя> ai-eltex" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "$api_key" ]; then
|
||||||
|
echo -n "Проверка сохранённого Eltex ключа... "
|
||||||
|
_eltex_check_key "$api_key"
|
||||||
|
ret=$_API_RET
|
||||||
|
if [ $ret -eq 401 ]; then
|
||||||
|
rm -f "$key_file"
|
||||||
|
api_key=""
|
||||||
|
reauth=1
|
||||||
|
elif [ $ret -eq 429 ]; then
|
||||||
|
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||||
|
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||||
|
elif [ $ret -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
elif [ -z "${ELTEX_MODEL:-}" ] && ! _eltex_pick_models; then
|
||||||
|
# Ключ валиден, но glm-5.3 недоступна (не та группа доступа) - авто-логаут,
|
||||||
|
# как при 401: такой ключ не годится для дефолтного запуска, сразу просим новый.
|
||||||
|
echo " Сохранённый ключ удалён (авто-логаут)."
|
||||||
|
rm -f "$key_file"
|
||||||
|
api_key=""
|
||||||
|
reauth=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
|
||||||
|
echo -n "Хотите ввести новый Eltex ключ? [Y/n] "
|
||||||
|
read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$api_key" ]; then
|
||||||
|
echo "Ключ выдают админы: https://intdocs.eltex.loc/pages/viewpage.action?pageId=262329878"
|
||||||
|
read -r -p "Введите ваш Eltex LLM ключ (sk-...): " api_key
|
||||||
|
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
||||||
|
|
||||||
|
echo -n "Проверяю ключ... "
|
||||||
|
_eltex_check_key "$api_key"
|
||||||
|
ret=$_API_RET
|
||||||
|
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
|
||||||
|
if [ -z "${ELTEX_MODEL:-}" ] && ! _eltex_pick_models; then
|
||||||
|
echo "Ключ НЕ сохранён."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$(dirname "$key_file")"
|
||||||
|
echo "$api_key" > "$key_file"
|
||||||
|
chmod 600 "$key_file"
|
||||||
|
echo "Ключ сохранён."
|
||||||
|
if [ $ret -eq 429 ]; then
|
||||||
|
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||||
|
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Ключ НЕ сохранён."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Модель: ELTEX_MODEL, иначе glm-5.3. Claude Code ходит в сырой путь прокси /anthropic: он
|
||||||
|
# байт-в-байт пересылает запрос на Anthropic-эндпоинт Z.ai (api.z.ai/api/anthropic), как у
|
||||||
|
# ai-glm, поэтому картинки работают через серверный analyze_image (20.10: Read png ->
|
||||||
|
# server_tool_use analyze_image -> верный ответ, так же, как напрямую). Обычный /v1/messages
|
||||||
|
# прокси для этого не годится: LiteLLM в нём режет картинки. Первую модель из
|
||||||
|
# /v1/models НЕ берём: там бывают не те модели и имена групп доступа (ZAI-Test-Group),
|
||||||
|
# которые не запустить. Если у ключа нет glm-5.3 - авто-логаут выше (свежевведённый такой
|
||||||
|
# ключ не сохраняется), вместо 400/403 внутри Claude Code. Модель не выбиралась
|
||||||
|
# (429/сетевая ошибка или задан ELTEX_MODEL) - выбираем здесь.
|
||||||
|
[ -n "${model:-}" ] || _eltex_pick_models 2>/dev/null || true
|
||||||
|
model="${ELTEX_MODEL:-${model:-glm-5.3}}"
|
||||||
|
# Окно 1M через суффикс [1m], а не глобальным CLAUDE_CODE_MAX_CONTEXT_TOKENS (у glm-5.3 на
|
||||||
|
# Anthropic-эндпоинте Z.ai 07.10 прошёл запрос на 598K); суффикс Claude Code в API не шлёт.
|
||||||
|
opus_model="$model"
|
||||||
|
case "$model" in glm-5.3*) opus_model="${model}[1m]" ;; esac
|
||||||
|
echo "Модель: $model"
|
||||||
|
|
||||||
|
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||||||
|
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||||||
|
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||||||
|
export AI_LAUNCHER=eltex
|
||||||
|
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex"
|
||||||
|
# Пикер: все слоты (opus, sonnet, haiku) = glm-5.3, других моделей ai-eltex не использует.
|
||||||
|
# Незаданный слот показал бы чужой Claude, а haiku Claude Code берёт и для фоновых задач
|
||||||
|
# вроде заголовков. availableModels НЕ задаём (он схлопывает пикер).
|
||||||
|
# Effort как у ai-glm: старт на max через CLI --effort (перебивает effortLevel из
|
||||||
|
# settings.json; AI_EFFORT=max в settings Claude Code не принимает и шлёт high), /effort
|
||||||
|
# в сессии работает. Z.ai учитывает output_config.effort (20.10 через /anthropic):
|
||||||
|
# low - без размышлений, дальше medium < high < max.
|
||||||
|
# CLAUDE_CODE_ALWAYS_ENABLE_EFFORT - страховка, как в ai-glm.
|
||||||
|
# Имена-алиасы claude-* на прокси НЕ используем: claude-3-5-sonnet Claude Code считает
|
||||||
|
# старым Claude 3.5 Sonnet (без effort и thinking, max_tokens=8192); у claude-5-5-sonnet
|
||||||
|
# один деплоймент на всех, модели говорят, что она Claude Sonnet, а statusline показывает
|
||||||
|
# аккаунт Claude.ai. На качество имя не влияет (09.10, одинаковый запрос с разной строкой
|
||||||
|
# "You are powered by": разброс внутри одного имени больше разницы между именами).
|
||||||
|
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 вместо дефолтных 32000, как у ai-glm.
|
||||||
|
# API_TIMEOUT_MS из доки админов: прокси бывает медленным.
|
||||||
|
# Окно glm-5.3 - 1M через [1m] (см. выше). Автокомпакт на 200K из доки админов и
|
||||||
|
# CLAUDE_CODE_DISABLE_1M_CONTEXT не ставим: ctx: показывал бы занятость в ~5 раз больше
|
||||||
|
# реальной.
|
||||||
|
# ENABLE_TOOL_SEARCH НЕ включаем: на Anthropic-эндпоинте Z.ai
|
||||||
|
# GLM с ним путается - ищет Read/Bash через ToolSearch и решает, что их нет. Цена -
|
||||||
|
# схемы встроенных тулов в каждом запросе (~+11K входных токенов).
|
||||||
|
_setup_isolated_config eltex opus xhigh ''
|
||||||
|
# /logout-eltex: без зарегистрированной команды Claude Code отвечает "Unknown command" и
|
||||||
|
# не зовёт UserPromptSubmit-хук (logout-account-hook.sh), а встроенный /logout хук обходит.
|
||||||
|
# Заглушка-команда в конфиге лаунчера пропускает /logout-eltex до хука, он удаляет ключ.
|
||||||
|
mkdir -p "$CLAUDE_CONFIG_DIR/commands"
|
||||||
|
cat > "$CLAUDE_CONFIG_DIR/commands/logout-eltex.md" <<'CMDEOF'
|
||||||
|
---
|
||||||
|
description: Удалить сохранённый ключ ai-eltex (обрабатывает хук, без LLM)
|
||||||
|
---
|
||||||
|
Хук не сработал: удали ~/.config/ai-setup/eltex_key вручную и перезапусти ai-eltex (или запусти AI_RESET_KEY=1 ai-eltex).
|
||||||
|
CMDEOF
|
||||||
|
ANTHROPIC_BASE_URL="$eltex_base_url/anthropic" \
|
||||||
|
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL="$opus_model" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="$model" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="GLM 5.3: картинки, effort, окно 1M" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL="$opus_model" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="$model" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="GLM 5.3: картинки, effort, окно 1M" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL="$opus_model" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="$model" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="GLM 5.3: картинки, effort, окно 1M" \
|
||||||
|
CLAUDE_CODE_SUBAGENT_MODEL="$opus_model" \
|
||||||
|
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
|
||||||
|
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
|
||||||
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||||
|
API_TIMEOUT_MS=600000 \
|
||||||
|
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||||
|
ELTEXEOF
|
||||||
|
chmod +x "$BIN_DIR/ai-eltex"
|
||||||
|
|
||||||
|
# === ai-eltex-qwen ===
|
||||||
|
cat > "$BIN_DIR/ai-eltex-qwen" << 'ELTEXQWENEOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ai-eltex-qwen - запуск Claude Code через корпоративный LLM-прокси Eltex на моделях Qwen
|
||||||
|
# (Eltex-Coder-Senior = Qwen3.8-27B, Eltex-Qwen3_8-Flash-Next = Qwen3.8-Flash-Next-FP8).
|
||||||
|
# Ключ отдельный от ai-eltex, его выдают админы.
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
key_file="$HOME/.config/ai-setup/eltex_qwen_key"
|
||||||
|
api_key=""
|
||||||
|
reauth=0
|
||||||
|
|
||||||
|
# Принудительный сброс сохранённого ключа через AI_RESET_KEY=1
|
||||||
|
if [ "${AI_RESET_KEY:-0}" == "1" ] && [ -f "$key_file" ]; then
|
||||||
|
rm -f "$key_file"
|
||||||
|
api_key=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$key_file" ] && api_key=$(cat "$key_file")
|
||||||
|
# Ключ из онбординга Eltex AI (переменная для opencode), если своего файла ещё нет
|
||||||
|
if [ -z "$api_key" ] && [ "${AI_RESET_KEY:-0}" != "1" ]; then
|
||||||
|
api_key="${AI_ELTEX_LLM_API_KEY:-}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
eltex_base_url="${ELTEX_BASE_URL:-http://llm.eltex.loc:4000}"
|
||||||
|
|
||||||
|
# _eltex_check_key: проверка ключа через GET /v1/models (как в ai-eltex), заодно отдаёт
|
||||||
|
# модели, разрешённые ключу.
|
||||||
|
_eltex_check_key() {
|
||||||
|
local response
|
||||||
|
# Без "|| echo 000": при сетевой ошибке -w сам печатает 000.
|
||||||
|
response=$(curl -s -w "\n%{http_code}" --max-time 15 "$eltex_base_url/v1/models" \
|
||||||
|
-H "Authorization: Bearer $1" \
|
||||||
|
2>/dev/null)
|
||||||
|
_CLAUDE_TEST_CODE=$(echo "$response" | tail -1)
|
||||||
|
_CLAUDE_TEST_BODY=$(echo "$response" | sed '$d')
|
||||||
|
if [ "$_CLAUDE_TEST_CODE" = "000" ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[0;31m[СЕТЬ]\033[0m Прокси $eltex_base_url недоступен."
|
||||||
|
echo " Нужна сеть Eltex. Если не резолвится llm.eltex.loc - перезапусти ru-bypass (добавит его в /etc/hosts)"
|
||||||
|
echo " или задай адрес явно: ELTEX_BASE_URL=http://172.16.4.68:4000 ai-eltex-qwen"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
_handle_openai_api_response "Eltex LLM" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Лимит исчерпан - обратись к админам"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "$api_key" ]; then
|
||||||
|
echo -n "Проверка сохранённого Eltex Qwen ключа... "
|
||||||
|
_eltex_check_key "$api_key"
|
||||||
|
ret=$_API_RET
|
||||||
|
if [ $ret -eq 401 ]; then
|
||||||
|
rm -f "$key_file"
|
||||||
|
api_key=""
|
||||||
|
reauth=1
|
||||||
|
elif [ $ret -eq 429 ]; then
|
||||||
|
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||||
|
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||||
|
elif [ $ret -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
|
||||||
|
echo -n "Хотите ввести новый Eltex Qwen ключ? [Y/n] "
|
||||||
|
read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$api_key" ]; then
|
||||||
|
echo "Ключ выдают админы (ключ для Eltex-Coder-Senior, не GLM-ключ от ai-eltex)."
|
||||||
|
echo "Можно задать его в AI_ELTEX_LLM_API_KEY, как в онбординге Eltex AI."
|
||||||
|
read -r -p "Введите ваш Eltex Qwen ключ (sk-...): " api_key
|
||||||
|
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
||||||
|
|
||||||
|
echo -n "Проверяю ключ... "
|
||||||
|
_eltex_check_key "$api_key"
|
||||||
|
ret=$_API_RET
|
||||||
|
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
|
||||||
|
mkdir -p "$(dirname "$key_file")"
|
||||||
|
echo "$api_key" > "$key_file"
|
||||||
|
chmod 600 "$key_file"
|
||||||
|
echo "Ключ сохранён."
|
||||||
|
if [ $ret -eq 429 ]; then
|
||||||
|
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||||
|
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Ключ НЕ сохранён."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Модель: ELTEX_QWEN_MODEL, иначе Eltex-Coder-Senior, если она есть у ключа, иначе
|
||||||
|
# Eltex-Qwen3_8-Flash-Next. Эмбеддер/реранкер из /v1/models для чата не годятся.
|
||||||
|
model="${ELTEX_QWEN_MODEL:-}"
|
||||||
|
if [ -z "$model" ]; then
|
||||||
|
model=$(echo "$_CLAUDE_TEST_BODY" | python3 -c '
|
||||||
|
import sys, json
|
||||||
|
ids = [m["id"] for m in json.load(sys.stdin).get("data", [])]
|
||||||
|
print(next((m for m in ("Eltex-Coder-Senior", "Eltex-Qwen3_8-Flash-Next") if m in ids), ""))' 2>/dev/null)
|
||||||
|
fi
|
||||||
|
model="${model:-Eltex-Coder-Senior}"
|
||||||
|
echo "Модель: $model"
|
||||||
|
|
||||||
|
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||||||
|
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||||||
|
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||||||
|
export AI_LAUNCHER=eltex-qwen
|
||||||
|
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex-qwen"
|
||||||
|
# Пикер: все три слота = модель ключа (незаданный слот показал бы чужой Claude).
|
||||||
|
# Окно 200K: у Qwen3.8-Flash-Next max_input_tokens=204800. CLAUDE_CODE_DISABLE_1M_CONTEXT
|
||||||
|
# обязателен: без него Claude Code на чистом конфиге переводит opus в opus[1m] (окно 1M).
|
||||||
|
# Effort прокси игнорирует (08.10: low/high = 3227/3297, 09.10: low/max = 4424-5838/
|
||||||
|
# 977-3370 выходных токенов), /effort ни на что не влияет. Стартуем на max через CLI
|
||||||
|
# --effort, как ai-glm и ai-eltex. Картинки Qwen3.8-Flash-Next видит сама.
|
||||||
|
# max_output_tokens=8192 из /models прокси не применяет (09.10 принят max_tokens=64000),
|
||||||
|
# поэтому вывод поднят до 64000, как у ai-glm и ai-eltex.
|
||||||
|
_setup_isolated_config eltex-qwen opus high ''
|
||||||
|
ANTHROPIC_BASE_URL="$eltex_base_url" \
|
||||||
|
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL="$model" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="$model" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL="$model" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="$model" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL="$model" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="$model" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
|
||||||
|
CLAUDE_CODE_SUBAGENT_MODEL="$model" \
|
||||||
|
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
|
||||||
|
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
|
||||||
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||||
|
API_TIMEOUT_MS=600000 \
|
||||||
|
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||||
|
ELTEXQWENEOF
|
||||||
|
chmod +x "$BIN_DIR/ai-eltex-qwen"
|
||||||
|
|
||||||
|
# === ai-mimo ===
|
||||||
|
cat > "$BIN_DIR/ai-mimo" << 'MIMOEOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ai-mimo - запуск Claude Code через Xiaomi MiMo V2.6 (Anthropic-compatible API, Token Plan)
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
key_file="$HOME/.config/ai-setup/mimo_key"
|
||||||
|
url_file="$HOME/.config/ai-setup/mimo_base_url"
|
||||||
|
api_key=""
|
||||||
|
reauth=0
|
||||||
|
|
||||||
|
# Принудительный сброс сохранённого ключа через AI_RESET_KEY=1
|
||||||
|
if [ "${AI_RESET_KEY:-0}" == "1" ]; then
|
||||||
|
rm -f "$key_file" "$url_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$key_file" ] && api_key=$(cat "$key_file")
|
||||||
|
|
||||||
|
# Ключ Token Plan (tp-...) работает только на эндпоинте региона подписки (sgp/ams/cn),
|
||||||
|
# ключ pay-as-you-go (sk-...) - на api.xiaomimimo.com. Эндпоинт находим перебором при
|
||||||
|
# проверке ключа и запоминаем в mimo_base_url; MIMO_BASE_URL перебивает.
|
||||||
|
mimo_base_url="${MIMO_BASE_URL:-}"
|
||||||
|
[ -z "$mimo_base_url" ] && [ -f "$url_file" ] && mimo_base_url=$(cat "$url_file")
|
||||||
|
|
||||||
|
# _mimo_check_key: 1-токенный запрос с ключом $1. Без известного эндпоинта перебирает
|
||||||
|
# регионы: чужой регион отвечает 401/403, первый другой ответ задаёт mimo_base_url.
|
||||||
|
# Недоступный эндпоинт (код 000*: _claude_test_api при сетевой ошибке отдаёт 000000)
|
||||||
|
# пропускаем. token-plan-cn в переборе нет: с наших машин он не отвечает (таймаут 15 с),
|
||||||
|
# для него MIMO_BASE_URL=https://token-plan-cn.xiaomimimo.com/anthropic.
|
||||||
|
# Bearer, как у Claude Code с ANTHROPIC_AUTH_TOKEN.
|
||||||
|
_mimo_check_key() {
|
||||||
|
local url auth_code="" auth_body=""
|
||||||
|
if [ -n "$mimo_base_url" ]; then
|
||||||
|
_claude_test_api "$mimo_base_url/v1/messages" "Authorization: Bearer $1" "mimo-v2.6-flash"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
for url in https://token-plan-sgp.xiaomimimo.com/anthropic \
|
||||||
|
https://token-plan-ams.xiaomimimo.com/anthropic \
|
||||||
|
https://api.xiaomimimo.com/anthropic; do
|
||||||
|
_claude_test_api "$url/v1/messages" "Authorization: Bearer $1" "mimo-v2.6-flash"
|
||||||
|
case "$_CLAUDE_TEST_CODE" in
|
||||||
|
401|403) auth_code="$_CLAUDE_TEST_CODE"; auth_body="$_CLAUDE_TEST_BODY" ;;
|
||||||
|
000*) ;;
|
||||||
|
*) mimo_base_url="$url"; return ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
# Ни один эндпоинт не принял ключ. Отказ в авторизации разберёт _handle_api_response,
|
||||||
|
# а без сети эндпоинт не узнать и запускать Claude Code некуда.
|
||||||
|
if [ -n "$auth_code" ]; then
|
||||||
|
_CLAUDE_TEST_CODE="$auth_code"
|
||||||
|
_CLAUDE_TEST_BODY="$auth_body"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[0;31m[СЕТЬ]\033[0m Эндпоинты MiMo недоступны, ключ не проверен."
|
||||||
|
echo " Задай эндпоинт явно: MIMO_BASE_URL=https://token-plan-sgp.xiaomimimo.com/anthropic ai-mimo"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# _mimo_check_result: разбор ответа проверки. 402 на pay-as-you-go эндпоинте значит ключ
|
||||||
|
# sk-... с нулевым балансом: подписку Token Plan он не тратит, для неё свой ключ tp-...
|
||||||
|
_mimo_check_result() {
|
||||||
|
_handle_api_response "MiMo" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Лимит Token Plan: https://platform.xiaomimimo.com"
|
||||||
|
if [ "$_API_RET" = "402" ] && [ "$mimo_base_url" = "https://api.xiaomimimo.com/anthropic" ]; then
|
||||||
|
echo " Это ключ pay-as-you-go (sk-...), подписку Token Plan он не использует."
|
||||||
|
echo " Для Token Plan создай ключ на странице Token Plan: Dedicated API Key -> Create API Key (tp-...)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "$api_key" ]; then
|
||||||
|
echo -n "Проверка сохранённого MiMo ключа... "
|
||||||
|
_mimo_check_key "$api_key"
|
||||||
|
_mimo_check_result
|
||||||
|
ret=$_API_RET
|
||||||
|
if [ $ret -eq 401 ]; then
|
||||||
|
rm -f "$key_file" "$url_file"
|
||||||
|
api_key=""
|
||||||
|
mimo_base_url="${MIMO_BASE_URL:-}"
|
||||||
|
reauth=1
|
||||||
|
elif [ $ret -eq 429 ]; then
|
||||||
|
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||||
|
read -r _ans
|
||||||
|
case "${_ans:-N}" in
|
||||||
|
[Yy]*) ;;
|
||||||
|
*)
|
||||||
|
echo -n "Хотите ввести новый MiMo ключ? [Y/n] "
|
||||||
|
read -r _ans2
|
||||||
|
case "${_ans2:-Y}" in
|
||||||
|
[Yy]*)
|
||||||
|
rm -f "$key_file" "$url_file"
|
||||||
|
api_key=""
|
||||||
|
mimo_base_url="${MIMO_BASE_URL:-}"
|
||||||
|
reauth=1
|
||||||
|
;;
|
||||||
|
*) exit 1 ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
elif [ $ret -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
|
||||||
|
echo -n "Хотите ввести новый MiMo ключ? [Y/n] "
|
||||||
|
read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$api_key" ]; then
|
||||||
|
echo "Получить ключ: https://platform.xiaomimimo.com (Token Plan - tp-..., pay-as-you-go - sk-...)"
|
||||||
|
read -r -p "Введите ваш Xiaomi MiMo API ключ: " api_key
|
||||||
|
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
||||||
|
|
||||||
|
echo -n "Проверяю ключ... "
|
||||||
|
_mimo_check_key "$api_key"
|
||||||
|
_mimo_check_result
|
||||||
|
ret=$_API_RET
|
||||||
|
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
|
||||||
|
mkdir -p "$(dirname "$key_file")"
|
||||||
|
echo "$api_key" > "$key_file"
|
||||||
|
echo "$mimo_base_url" > "$url_file"
|
||||||
|
chmod 600 "$key_file" "$url_file"
|
||||||
|
echo "Ключ сохранён (эндпоинт: $mimo_base_url)."
|
||||||
|
if [ $ret -eq 429 ]; then
|
||||||
|
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
|
||||||
|
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Ключ НЕ сохранён."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||||||
|
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||||||
|
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||||||
|
export AI_LAUNCHER=mimo
|
||||||
|
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/mimo"
|
||||||
|
# Модель: всегда самая умная MiMo - mimo-v2.6-pro во всех трёх слотах и в сабагентах, как
|
||||||
|
# у ai-glm. Flash не используем: ai-mimo задуман как максимум качества, а не экономия
|
||||||
|
# кредитов. pro-ultraspeed - тот же Pro, только быстрее и в ~10 раз дороже.
|
||||||
|
# mimo-v2.5-pro/mimo-v2.5 сняты 21.10.2026 (с 14.10 их роутят на v2.6-pro/v2.6-flash):
|
||||||
|
# https://mimo.mi.com/docs/en-US/updates/deprecate
|
||||||
|
# availableModels НЕ задаём (он схлопывает пикер). --model opus при каждом запуске: выбор
|
||||||
|
# через /model живёт до конца сессии, следующий запуск снова на Pro.
|
||||||
|
# CLAUDE_CODE_MAX_CONTEXT_TOKENS: mimo-* нет в реестре моделей Claude Code, без env окно
|
||||||
|
# считалось бы = 200K (ctx: врал бы в ~5 раз, автокомпакт жал бы сессию). У Pro реальное
|
||||||
|
# окно 1M, поэтому CLAUDE_CODE_DISABLE_1M_CONTEXT не выставляем.
|
||||||
|
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000: потолок Claude Code (131072 он режет до 128000),
|
||||||
|
# MiMo V2.6 отдаёт до 131072. По умолчанию незнакомой модели досталось бы 32000 на
|
||||||
|
# размышления + ответ.
|
||||||
|
# Размышления: MiMo думает всегда. Claude Code шлёт thinking adaptive, а MiMo отрабатывает
|
||||||
|
# его так же, как enabled (замер 13.10: даже на "скажи привет" есть thinking, длина та же).
|
||||||
|
# Effort MiMo игнорирует: Claude Code шлёт output_config.effort (и без
|
||||||
|
# CLAUDE_CODE_ALWAYS_ENABLE_EFFORT), MiMo принимает даже effort=bogus, а длина размышлений
|
||||||
|
# от low до max не меняется (см. EFFORT_MAPPING.md). Стартуем на max через CLI --effort,
|
||||||
|
# как ai-eltex-qwen: если Xiaomi начнёт его учитывать, будет максимум.
|
||||||
|
_setup_isolated_config mimo opus high ''
|
||||||
|
# /logout-mimo: без зарегистрированной команды Claude Code отвечает "Unknown command" и
|
||||||
|
# не зовёт UserPromptSubmit-хук (см. ai-eltex).
|
||||||
|
mkdir -p "$CLAUDE_CONFIG_DIR/commands"
|
||||||
|
cat > "$CLAUDE_CONFIG_DIR/commands/logout-mimo.md" <<'CMDEOF'
|
||||||
|
---
|
||||||
|
description: Удалить сохранённый ключ ai-mimo (обрабатывает хук, без LLM)
|
||||||
|
---
|
||||||
|
Хук не сработал: удали ~/.config/ai-setup/mimo_key вручную и перезапусти ai-mimo (или запусти AI_RESET_KEY=1 ai-mimo).
|
||||||
|
CMDEOF
|
||||||
|
ANTHROPIC_BASE_URL="$mimo_base_url" \
|
||||||
|
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL=mimo-v2.6-pro \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="MiMo V2.6 Pro" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="Xiaomi MiMo V2.6 Pro - флагман для сложных задач" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL=mimo-v2.6-pro \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="MiMo V2.6 Pro" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Xiaomi MiMo V2.6 Pro - флагман для сложных задач" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL=mimo-v2.6-pro \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="MiMo V2.6 Pro" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="Xiaomi MiMo V2.6 Pro - флагман для сложных задач" \
|
||||||
|
CLAUDE_CODE_SUBAGENT_MODEL=mimo-v2.6-pro \
|
||||||
|
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
|
||||||
|
CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000 \
|
||||||
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||||
|
claude --dangerously-skip-permissions --model opus --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||||
|
MIMOEOF
|
||||||
|
chmod +x "$BIN_DIR/ai-mimo"
|
||||||
|
|
||||||
|
# === ai-qwen ===
|
||||||
|
cat > "$BIN_DIR/ai-qwen" << 'QWENEOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ai-qwen - запуск Claude Code через Alibaba Cloud Model Studio (Qwen 3.8, Token Plan,
|
||||||
|
# Anthropic-compatible API). Ключ общий и вшит в скрипт: запускается сразу, без ввода ключа.
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
qwen_base_url="https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic"
|
||||||
|
api_key="sk-sp-H.LEDIL.UJfr.MEUCIBmN5Rtjyn9iKPnhw2sym_1auFL4YZ4tnJh54TurNMo8AiEA1SbkS2Gt872ncq-32xK2ZxlpZxS3ldNHnTgX6dNCIok"
|
||||||
|
|
||||||
|
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||||||
|
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||||||
|
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||||||
|
export AI_LAUNCHER=qwen
|
||||||
|
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/qwen"
|
||||||
|
# Модели (список ключа: .../compatible-mode/v1/models): qwen3.8-max - самая умная, она в
|
||||||
|
# opus/sonnet и в сабагентах; haiku = qwen3.8-flash (быстрая, Claude Code берёт слот haiku
|
||||||
|
# и для фоновых задач вроде заголовков - так дешевле для лимита Token Plan).
|
||||||
|
# availableModels НЕ задаём (он схлопывает пикер). --model opus при каждом запуске, как в ai-mimo.
|
||||||
|
# Окно: у обеих моделей вход до 983616 токенов (замер 20.10: ответ "Range of input length
|
||||||
|
# should be [1, 983616]"). qwen3.8-* нет в реестре Claude Code, без
|
||||||
|
# CLAUDE_CODE_MAX_CONTEXT_TOKENS окно считалось бы = 200K. Автокомпакт с окном 1000000
|
||||||
|
# срабатывает раньше предела API. Картинки обе модели видят сами.
|
||||||
|
# Effort Qwen проверяет (bogus -> 400, low/medium/high/xhigh/max принимаются), но у Max тренда
|
||||||
|
# нет, у Flash max размышляет почти вдвое дольше low (см. EFFORT_MAPPING.md). Стартуем на max
|
||||||
|
# через CLI --effort, как ai-mimo. CLAUDE_CODE_ALWAYS_ENABLE_EFFORT - страховка, как в ai-glm.
|
||||||
|
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 вместо дефолтных 32000, как у ai-glm.
|
||||||
|
_setup_isolated_config qwen opus high ''
|
||||||
|
ANTHROPIC_BASE_URL="$qwen_base_url" \
|
||||||
|
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3.8-max \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="Qwen3.8 Max" \
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="Qwen3.8 Max - флагман Alibaba для сложных задач" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3.8-max \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="Qwen3.8 Max" \
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Qwen3.8 Max - флагман Alibaba для сложных задач" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3.8-flash \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="Qwen3.8 Flash" \
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="Qwen3.8 Flash - быстрая" \
|
||||||
|
CLAUDE_CODE_SUBAGENT_MODEL=qwen3.8-max \
|
||||||
|
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
|
||||||
|
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
|
||||||
|
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
|
||||||
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||||
|
claude --dangerously-skip-permissions --model opus --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
|
||||||
|
QWENEOF
|
||||||
|
chmod +x "$BIN_DIR/ai-qwen"
|
||||||
|
|
||||||
# === ai-openrouter ===
|
# === ai-openrouter ===
|
||||||
cat > "$BIN_DIR/ai-openrouter" << 'OPENROUTEREOF'
|
cat > "$BIN_DIR/ai-openrouter" << 'OPENROUTEREOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@@ -1982,7 +2559,11 @@ chmod +x "$BIN_DIR/ai-opencode"
|
|||||||
source "$BIN_DIR/ai-api-helpers.sh"
|
source "$BIN_DIR/ai-api-helpers.sh"
|
||||||
_setup_isolated_config deepseek opus high ''
|
_setup_isolated_config deepseek opus high ''
|
||||||
_setup_isolated_config kimi opus high ''
|
_setup_isolated_config kimi opus high ''
|
||||||
_setup_isolated_config glm opus high ''
|
_setup_isolated_config glm opus xhigh ''
|
||||||
|
_setup_isolated_config eltex opus xhigh ''
|
||||||
|
_setup_isolated_config eltex-qwen opus high ''
|
||||||
|
_setup_isolated_config mimo opus high ''
|
||||||
|
_setup_isolated_config qwen opus high ''
|
||||||
_setup_isolated_config openrouter "openai/gpt-5.5" high ''
|
_setup_isolated_config openrouter "openai/gpt-5.5" high ''
|
||||||
_setup_isolated_config opencode deepseek-v4-pro high ''
|
_setup_isolated_config opencode deepseek-v4-pro high ''
|
||||||
|
|
||||||
@@ -1993,7 +2574,7 @@ rm -f "$BIN_DIR/claude_gpt" "$BIN_DIR/claude_deepseek" "$BIN_DIR/claude_kimi" "$
|
|||||||
if [ "$BIN_DIR" != "$HOME/.local/bin" ]; then
|
if [ "$BIN_DIR" != "$HOME/.local/bin" ]; then
|
||||||
warn "BIN_DIR=$BIN_DIR — удаляю старые скрипты из ~/.local/bin/ ..."
|
warn "BIN_DIR=$BIN_DIR — удаляю старые скрипты из ~/.local/bin/ ..."
|
||||||
rm -f "$HOME/.local/bin/ai-gpt" "$HOME/.local/bin/ai-deepseek" "$HOME/.local/bin/ai-kimi" \
|
rm -f "$HOME/.local/bin/ai-gpt" "$HOME/.local/bin/ai-deepseek" "$HOME/.local/bin/ai-kimi" \
|
||||||
"$HOME/.local/bin/ai-glm" "$HOME/.local/bin/ai-openrouter" "$HOME/.local/bin/ai-gemini" "$HOME/.local/bin/ai-cursor" "$HOME/.local/bin/ai-claude" \
|
"$HOME/.local/bin/ai-glm" "$HOME/.local/bin/ai-eltex" "$HOME/.local/bin/ai-eltex-qwen" "$HOME/.local/bin/ai-mimo" "$HOME/.local/bin/ai-qwen" "$HOME/.local/bin/ai-openrouter" "$HOME/.local/bin/ai-gemini" "$HOME/.local/bin/ai-cursor" "$HOME/.local/bin/ai-claude" \
|
||||||
"$HOME/.local/bin/ai-opencode" \
|
"$HOME/.local/bin/ai-opencode" \
|
||||||
"$HOME/.local/bin/ai-api-helpers.sh" "$HOME/.local/bin/claude-gpt-effort-proxy.py"
|
"$HOME/.local/bin/ai-api-helpers.sh" "$HOME/.local/bin/claude-gpt-effort-proxy.py"
|
||||||
fi
|
fi
|
||||||
@@ -2013,6 +2594,10 @@ echo -e " ${CYAN}ai-claude${NC} - Оригинальный Claude Co
|
|||||||
echo -e " ${CYAN}ai-deepseek${NC} - DeepSeek (через Claude Code, API ключ сохраняется)"
|
echo -e " ${CYAN}ai-deepseek${NC} - DeepSeek (через Claude Code, API ключ сохраняется)"
|
||||||
echo -e " ${CYAN}ai-kimi${NC} - Kimi K2.7 Code (через Claude Code, API ключ сохраняется)"
|
echo -e " ${CYAN}ai-kimi${NC} - Kimi K2.7 Code (через Claude Code, API ключ сохраняется)"
|
||||||
echo -e " ${CYAN}ai-glm${NC} - GLM 5.3 (через Claude Code, Zhipu AI API)"
|
echo -e " ${CYAN}ai-glm${NC} - GLM 5.3 (через Claude Code, Zhipu AI API)"
|
||||||
|
echo -e " ${CYAN}ai-eltex${NC} - Корпоративный LLM-прокси Eltex (через Claude Code, ключ от админов)"
|
||||||
|
echo -e " ${CYAN}ai-eltex-qwen${NC} - Корпоративный LLM-прокси Eltex, модели Qwen (через Claude Code, свой ключ)"
|
||||||
|
echo -e " ${CYAN}ai-mimo${NC} - Xiaomi MiMo V2.6 Pro, максимум рассуждений (через Claude Code, Token Plan)"
|
||||||
|
echo -e " ${CYAN}ai-qwen${NC} - Qwen3.8 Max (через Claude Code, Alibaba Token Plan, ключ встроен)"
|
||||||
echo -e " ${CYAN}ai-openrouter${NC} - OpenRouter (через Claude Code: GPT-5.5, Grok, Qwen, GLM 5.2 и др.)"
|
echo -e " ${CYAN}ai-openrouter${NC} - OpenRouter (через Claude Code: GPT-5.5, Grok, Qwen, GLM 5.2 и др.)"
|
||||||
echo -e " ${CYAN}ai-opencode${NC} - OpenCode Go (через Claude Code + routatic-proxy: DeepSeek, Kimi, Qwen, GLM, MiniMax, MiMo)"
|
echo -e " ${CYAN}ai-opencode${NC} - OpenCode Go (через Claude Code + routatic-proxy: DeepSeek, Kimi, Qwen, GLM, MiniMax, MiMo)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -162,10 +162,12 @@ fi
|
|||||||
# Без этого DNS через VPN может вернуть внешний IP вместо внутреннего,
|
# Без этого DNS через VPN может вернуть внешний IP вместо внутреннего,
|
||||||
# и трафик пойдёт через VPN вместо прямого соединения.
|
# и трафик пойдёт через VPN вместо прямого соединения.
|
||||||
HOSTS_MARKER="# ru-bypass: local hosts"
|
HOSTS_MARKER="# ru-bypass: local hosts"
|
||||||
|
HOSTS_END_MARKER="# ru-bypass: end"
|
||||||
|
|
||||||
# Удаляем старый блок целиком (от маркера до конца файла)
|
# Удаляем только свой блок (от маркера до конечного маркера).
|
||||||
# и заодно чистим дубликаты от старых версий скрипта (без маркера)
|
# Записи, добавленные вручную вне блока, не трогаем.
|
||||||
sed -i "/$HOSTS_MARKER/,\$ d; /eltex\.loc/d; /eltex-co\.ru/d" /etc/hosts
|
# Блок старых версий без конечного маркера удаляется до конца файла.
|
||||||
|
sed -i "/^$HOSTS_MARKER\$/,/^$HOSTS_END_MARKER\$/d" /etc/hosts
|
||||||
|
|
||||||
# Добавляем актуальные
|
# Добавляем актуальные
|
||||||
cat >> /etc/hosts <<_HOSTS
|
cat >> /etc/hosts <<_HOSTS
|
||||||
@@ -180,8 +182,10 @@ $HOSTS_MARKER
|
|||||||
172.16.5.63 proxy.eltex.loc
|
172.16.5.63 proxy.eltex.loc
|
||||||
10.80.0.16 ssw.eltex.loc
|
10.80.0.16 ssw.eltex.loc
|
||||||
172.16.5.78 nexus.eltex.loc
|
172.16.5.78 nexus.eltex.loc
|
||||||
|
172.16.4.68 llm.eltex.loc llm2.eltex.loc
|
||||||
172.16.1.149 cpe-worker.eltex.loc
|
172.16.1.149 cpe-worker.eltex.loc
|
||||||
172.16.5.22 mattermost.eltex-co.ru elph.eltex-co.ru ecss-elph-proxy.eltex-co.ru study-git.eltex-co.ru study-red.eltex-co.ru
|
172.16.5.22 mattermost.eltex-co.ru elph.eltex-co.ru ecss-elph-proxy.eltex-co.ru study-git.eltex-co.ru study-red.eltex-co.ru
|
||||||
|
$HOSTS_END_MARKER
|
||||||
_HOSTS
|
_HOSTS
|
||||||
|
|
||||||
echo "Локальные хосты: *.eltex.loc, mattermost, elph → /etc/hosts"
|
echo "Локальные хосты: *.eltex.loc, mattermost, elph → /etc/hosts"
|
||||||
@@ -241,7 +245,8 @@ if [ -n "${ALL_EXC// }" ]; then
|
|||||||
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
|
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
|
||||||
ips="$item"
|
ips="$item"
|
||||||
else
|
else
|
||||||
ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
ips=$(getent ahostsv4 "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
||||||
|
[ -z "$ips" ] && ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
||||||
fi
|
fi
|
||||||
for ip in $ips; do
|
for ip in $ips; do
|
||||||
ipset add ru-direct "$ip" -exist 2>/dev/null || true
|
ipset add ru-direct "$ip" -exist 2>/dev/null || true
|
||||||
@@ -295,7 +300,8 @@ if [ -n "${ALL_EXC// }" ]; then
|
|||||||
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
|
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
|
||||||
ips="$item"
|
ips="$item"
|
||||||
else
|
else
|
||||||
ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
ips=$(getent ahostsv4 "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
||||||
|
[ -z "$ips" ] && ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
|
||||||
fi
|
fi
|
||||||
for ip in $ips; do
|
for ip in $ips; do
|
||||||
ip route replace "$ip/32" via "$GATEWAY" dev "$DEV" 2>/dev/null
|
ip route replace "$ip/32" via "$GATEWAY" dev "$DEV" 2>/dev/null
|
||||||
|
|||||||
6
setup.sh
6
setup.sh
@@ -230,7 +230,7 @@ case "$choice" in
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${BLD}AI инструменты:${CLR}"
|
echo -e "${BLD}AI инструменты:${CLR}"
|
||||||
for cmd in ai-claude ai-gpt ai-deepseek ai-kimi ai-glm ai-openrouter ai-opencode ai-gemini android; do
|
for cmd in ai-claude ai-gpt ai-deepseek ai-kimi ai-glm ai-eltex ai-eltex-qwen ai-mimo ai-qwen ai-openrouter ai-opencode ai-gemini android; do
|
||||||
if command -v "$cmd" &>/dev/null; then
|
if command -v "$cmd" &>/dev/null; then
|
||||||
echo -e " ${GRN}✓${CLR} $cmd"
|
echo -e " ${GRN}✓${CLR} $cmd"
|
||||||
else
|
else
|
||||||
@@ -241,7 +241,7 @@ case "$choice" in
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLD}API ключи:${CLR}"
|
echo -e "${BLD}API ключи:${CLR}"
|
||||||
cfg="$HOME/.config/ai-setup"
|
cfg="$HOME/.config/ai-setup"
|
||||||
for f in deepseek_key kimi_key glm_key openrouter_key opencode_key; do
|
for f in deepseek_key kimi_key glm_key eltex_key eltex_qwen_key mimo_key openrouter_key opencode_key; do
|
||||||
name="${f/_key/}"
|
name="${f/_key/}"
|
||||||
if [ -s "$cfg/$f" ]; then
|
if [ -s "$cfg/$f" ]; then
|
||||||
echo -e " ${GRN}✓${CLR} $name"
|
echo -e " ${GRN}✓${CLR} $name"
|
||||||
@@ -252,7 +252,7 @@ case "$choice" in
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLD}API доступность:${CLR}"
|
echo -e "${BLD}API доступность:${CLR}"
|
||||||
for entry in "Anthropic:api.anthropic.com" "DeepSeek:api.deepseek.com" "OpenAI:api.openai.com" "Kimi:api.kimi.com" "GLM:api.z.ai" "OpenRouter:openrouter.ai" "OpenCode:opencode.ai"; do
|
for entry in "Anthropic:api.anthropic.com" "DeepSeek:api.deepseek.com" "OpenAI:api.openai.com" "Kimi:api.kimi.com" "GLM:api.z.ai" "MiMo:api.xiaomimimo.com" "Qwen:token-plan.ap-southeast-1.maas.aliyuncs.com" "OpenRouter:openrouter.ai" "OpenCode:opencode.ai"; do
|
||||||
_name="${entry%%:*}"
|
_name="${entry%%:*}"
|
||||||
_host="${entry##*:}"
|
_host="${entry##*:}"
|
||||||
_ms=$(curl -s -o /dev/null -w "%{time_connect}" --max-time 5 "https://$_host" 2>/dev/null)
|
_ms=$(curl -s -o /dev/null -w "%{time_connect}" --max-time 5 "https://$_host" 2>/dev/null)
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); }
|
|||||||
GPT_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-gpt"/,/^GPTEOF/' "$SCRIPT")
|
GPT_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-gpt"/,/^GPTEOF/' "$SCRIPT")
|
||||||
KIMI_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-kimi"/,/^KIMIEOF/' "$SCRIPT")
|
KIMI_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-kimi"/,/^KIMIEOF/' "$SCRIPT")
|
||||||
GLM_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-glm"/,/^GLMEOF/' "$SCRIPT")
|
GLM_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-glm"/,/^GLMEOF/' "$SCRIPT")
|
||||||
|
ELTEX_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-eltex"/,/^ELTEXEOF/' "$SCRIPT")
|
||||||
|
ELTEX_QWEN_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-eltex-qwen"/,/^ELTEXQWENEOF/' "$SCRIPT")
|
||||||
|
MIMO_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-mimo"/,/^MIMOEOF/' "$SCRIPT")
|
||||||
|
QWEN_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-qwen"/,/^QWENEOF/' "$SCRIPT")
|
||||||
|
LOGOUT_HOOK="$(cd "$(dirname "$0")/.." && pwd)/home-configs/claude/hooks/logout-account-hook.sh"
|
||||||
GEMINI_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-gemini"/,/^GEMINIEOF/' "$SCRIPT")
|
GEMINI_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-gemini"/,/^GEMINIEOF/' "$SCRIPT")
|
||||||
OPENCODE_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-opencode"/,/^OPENCODEEOF/' "$SCRIPT")
|
OPENCODE_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-opencode"/,/^OPENCODEEOF/' "$SCRIPT")
|
||||||
CURSOR_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-cursor"/,/^CURSOREOF/' "$SCRIPT")
|
CURSOR_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-cursor"/,/^CURSOREOF/' "$SCRIPT")
|
||||||
@@ -104,6 +109,377 @@ test_glm_official_api() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── ai-eltex: Claude Code через корпоративный прокси Eltex ─────────────────
|
||||||
|
test_eltex_claude_launcher() {
|
||||||
|
if echo "$ELTEX_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
|
||||||
|
&& echo "$ELTEX_SECTION" | grep -q 'ELTEX_BASE_URL:-http://llm.eltex.loc:4000' \
|
||||||
|
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_BASE_URL="$eltex_base_url/anthropic"' \
|
||||||
|
&& echo "$ELTEX_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex"' \
|
||||||
|
&& echo "$ELTEX_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/eltex_key"'; then
|
||||||
|
ok "ai-eltex: launches claude via llm.eltex.loc with its own key and config dir"
|
||||||
|
else
|
||||||
|
fail "ai-eltex: must launch claude via llm.eltex.loc with eltex_key and cfg/eltex"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-eltex: модель берётся из /v1/models ключа ──────────────────────────
|
||||||
|
test_eltex_model_from_key() {
|
||||||
|
if echo "$ELTEX_SECTION" | grep -q '/v1/models' \
|
||||||
|
&& echo "$ELTEX_SECTION" | grep -Fq 'model="${ELTEX_MODEL:-${model:-glm-5.3}}"' \
|
||||||
|
&& echo "$ELTEX_SECTION" | grep -Fq 'pick("glm-5.3", "glm-5.3-", "flash")' \
|
||||||
|
&& ! echo "$ELTEX_SECTION" | grep -q 'ids\[0\]' \
|
||||||
|
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL="$opus_model"' \
|
||||||
|
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_SONNET_MODEL="$opus_model"' \
|
||||||
|
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL="$opus_model"'; then
|
||||||
|
ok "ai-eltex: glm-5.3 by default, never the first /v1/models entry (ELTEX_MODEL overrides)"
|
||||||
|
else
|
||||||
|
fail "ai-eltex: must default to glm-5.3 (not the first /v1/models entry), ELTEX_MODEL overrides"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-eltex: glm-5.3 во всех слотах пикера (имена с суффиксом тоже) ───────
|
||||||
|
test_eltex_three_slots() {
|
||||||
|
local tmp ok_run=1 out
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home/.config/ai-setup"
|
||||||
|
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
|
||||||
|
echo "$ELTEX_SECTION" | BIN_DIR="$tmp/bin" bash
|
||||||
|
cat > "$tmp/mock/curl" <<'CURLEOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n%s' "$MODELS_BODY" "200"
|
||||||
|
CURLEOF
|
||||||
|
cat > "$tmp/mock/claude" <<'CLAUDEEOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "SLOTS opus=$ANTHROPIC_DEFAULT_OPUS_MODEL sonnet=$ANTHROPIC_DEFAULT_SONNET_MODEL haiku=$ANTHROPIC_DEFAULT_HAIKU_MODEL sub=$CLAUDE_CODE_SUBAGENT_MODEL"
|
||||||
|
CLAUDEEOF
|
||||||
|
chmod +x "$tmp/mock/curl" "$tmp/mock/claude"
|
||||||
|
echo sk-test > "$tmp/home/.config/ai-setup/eltex_key"
|
||||||
|
# Как на прокси: flash идёт первой, плюс имя группы доступа - во всех слотах glm-5.3
|
||||||
|
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||||
|
MODELS_BODY='{"data":[{"id":"glm-5.3-flash"},{"id":"glm-5.3"},{"id":"ZAI-Test-Group"}]}' \
|
||||||
|
bash "$tmp/bin/ai-eltex" 2>&1)
|
||||||
|
echo "$out" | grep -Fq 'SLOTS opus=glm-5.3[1m] sonnet=glm-5.3[1m] haiku=glm-5.3[1m] sub=glm-5.3[1m]' || ok_run=0
|
||||||
|
# Имена с суффиксом: модель не путается с flash
|
||||||
|
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||||
|
MODELS_BODY='{"data":[{"id":"glm-5.3-flash-2"},{"id":"glm-5.3-2"}]}' \
|
||||||
|
bash "$tmp/bin/ai-eltex" 2>&1)
|
||||||
|
echo "$out" | grep -Fq 'SLOTS opus=glm-5.3-2[1m] sonnet=glm-5.3-2[1m] haiku=glm-5.3-2[1m]' || ok_run=0
|
||||||
|
if [ "$ok_run" -eq 1 ]; then
|
||||||
|
ok "ai-eltex: glm-5.3[1m] in every picker slot, flash never picked"
|
||||||
|
else
|
||||||
|
fail "ai-eltex: every picker slot must be glm-5.3[1m], flash must never be picked"
|
||||||
|
fi
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-eltex: нет glm-5.3 у ключа -> авто-логаут, свежий ключ не сохраняется ──
|
||||||
|
test_eltex_autologout_on_missing_model() {
|
||||||
|
local tmp ok_run=1 rc=0 out
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home/.config/ai-setup"
|
||||||
|
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
|
||||||
|
echo "$ELTEX_SECTION" | BIN_DIR="$tmp/bin" bash
|
||||||
|
# Прокси отвечает 200, но glm-5.3 в списке моделей ключа нет
|
||||||
|
cat > "$tmp/mock/curl" <<'CURLEOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
if [[ "$*" == *"/v1/models"* ]]; then
|
||||||
|
printf '%s\n%s' "$MODELS_BODY" "200"
|
||||||
|
else
|
||||||
|
printf '%s\n%s' '{"ok":true}' "200"
|
||||||
|
fi
|
||||||
|
CURLEOF
|
||||||
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$tmp/mock/claude"
|
||||||
|
chmod +x "$tmp/mock/curl" "$tmp/mock/claude"
|
||||||
|
# Сохранённый ключ: авто-логаут (файл удалён), новый ключ не вводим -> выход 1
|
||||||
|
echo sk-old > "$tmp/home/.config/ai-setup/eltex_key"
|
||||||
|
out=$(printf 'n\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||||
|
MODELS_BODY='{"data":[{"id":"glm-5.3-flash-2"},{"id":"ZAI-Test-Group"}]}' \
|
||||||
|
bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
|
||||||
|
[ "$rc" -eq 1 ] || ok_run=0
|
||||||
|
echo "$out" | grep -q 'Ключу недоступна модель glm-5.3' || ok_run=0
|
||||||
|
echo "$out" | grep -q 'авто-логаут' || ok_run=0
|
||||||
|
[ ! -f "$tmp/home/.config/ai-setup/eltex_key" ] || ok_run=0
|
||||||
|
# Свежий ключ без glm-5.3 не сохраняется вовсе
|
||||||
|
rc=0
|
||||||
|
out=$(printf 'sk-new\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||||
|
MODELS_BODY='{"data":[{"id":"glm-5.3-flash-2"}]}' \
|
||||||
|
bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
|
||||||
|
[ "$rc" -eq 1 ] || ok_run=0
|
||||||
|
echo "$out" | grep -q 'Ключ НЕ сохранён' || ok_run=0
|
||||||
|
[ ! -f "$tmp/home/.config/ai-setup/eltex_key" ] || ok_run=0
|
||||||
|
# ELTEX_MODEL перебивает проверку: ключ живёт, запуск идёт
|
||||||
|
echo sk-keep > "$tmp/home/.config/ai-setup/eltex_key"
|
||||||
|
rc=0
|
||||||
|
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||||
|
MODELS_BODY='{"data":[{"id":"glm-5.3-flash-2"}]}' \
|
||||||
|
ELTEX_MODEL=glm-5.3-2 bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
|
||||||
|
[ "$rc" -eq 0 ] || ok_run=0
|
||||||
|
echo "$out" | grep -q 'Модель: glm-5.3-2' || ok_run=0
|
||||||
|
[ "$(cat "$tmp/home/.config/ai-setup/eltex_key")" = "sk-keep" ] || ok_run=0
|
||||||
|
if [ "$ok_run" -eq 1 ]; then
|
||||||
|
ok "ai-eltex: missing glm-5.3 auto-logouts saved key, fresh key not saved, ELTEX_MODEL bypasses"
|
||||||
|
else
|
||||||
|
fail "ai-eltex: missing glm-5.3 must auto-logout (saved key removed, fresh key not saved, ELTEX_MODEL bypass)"
|
||||||
|
fi
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-eltex: без ENABLE_TOOL_SEARCH (на anthropic-пути GLM теряет Read/Bash) ──
|
||||||
|
test_eltex_no_tool_search() {
|
||||||
|
if echo "$ELTEX_SECTION" | grep -q '^ENABLE_TOOL_SEARCH='; then
|
||||||
|
fail "ai-eltex: must not force ENABLE_TOOL_SEARCH (GLM loses core tools on anthropic path)"
|
||||||
|
else
|
||||||
|
ok "ai-eltex: ENABLE_TOOL_SEARCH not forced"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-eltex: окно 1M, как у ai-glm (иначе ctx: завышен в ~5 раз) ─────────
|
||||||
|
test_eltex_context_window() {
|
||||||
|
if echo "$ELTEX_SECTION" | grep -Fq 'glm-5.3*) opus_model="${model}[1m]"' \
|
||||||
|
&& ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=' \
|
||||||
|
&& ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=' \
|
||||||
|
&& ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_AUTO_COMPACT_WINDOW='; then
|
||||||
|
ok "ai-eltex: 1M window for glm-5.3 via [1m], no global MAX_CONTEXT / DISABLE_1M"
|
||||||
|
else
|
||||||
|
fail "ai-eltex: 1M for glm-5.3 via [1m], no global CLAUDE_CODE_MAX_CONTEXT_TOKENS / DISABLE_1M / AUTO_COMPACT_WINDOW"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-eltex-qwen: свой ключ и конфиг, модели Qwen, окно 200K ─────────────
|
||||||
|
test_eltex_qwen_launcher() {
|
||||||
|
if echo "$ELTEX_QWEN_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
|
||||||
|
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/eltex_qwen_key"' \
|
||||||
|
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex-qwen"' \
|
||||||
|
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'model="${ELTEX_QWEN_MODEL:-}"' \
|
||||||
|
&& echo "$ELTEX_QWEN_SECTION" | grep -q '("Eltex-Coder-Senior", "Eltex-Qwen3_8-Flash-Next")' \
|
||||||
|
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'api_key="${AI_ELTEX_LLM_API_KEY:-}"' \
|
||||||
|
&& echo "$ELTEX_QWEN_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=1'; then
|
||||||
|
ok "ai-eltex-qwen: own key/config, Coder-Senior then Flash-Next, 200K window"
|
||||||
|
else
|
||||||
|
fail "ai-eltex-qwen: must use eltex_qwen_key, cfg/eltex-qwen, Qwen model preference and DISABLE_1M"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_logout_eltex_qwen() {
|
||||||
|
local tmp_home rc=0
|
||||||
|
tmp_home=$(mktemp -d)
|
||||||
|
mkdir -p "$tmp_home/.config/ai-setup"
|
||||||
|
echo "sk-test" > "$tmp_home/.config/ai-setup/eltex_qwen_key"
|
||||||
|
echo "sk-glm" > "$tmp_home/.config/ai-setup/eltex_key"
|
||||||
|
echo '{"prompt":"/logout"}' | HOME="$tmp_home" AI_LAUNCHER=eltex-qwen bash "$LOGOUT_HOOK" 2>/dev/null || rc=$?
|
||||||
|
if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/eltex_qwen_key" ] && [ -f "$tmp_home/.config/ai-setup/eltex_key" ]; then
|
||||||
|
ok "logout hook: /logout in ai-eltex-qwen removes only eltex_qwen_key"
|
||||||
|
else
|
||||||
|
fail "logout hook: /logout in ai-eltex-qwen must remove eltex_qwen_key and keep eltex_key (rc=$rc)"
|
||||||
|
fi
|
||||||
|
rm -rf "$tmp_home"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-eltex / ai-eltex-qwen: effort max по умолчанию через CLI, вывод 64000 ────
|
||||||
|
test_eltex_effort() {
|
||||||
|
if echo "$ELTEX_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \
|
||||||
|
&& echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1' \
|
||||||
|
&& echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \
|
||||||
|
&& ! echo "$ELTEX_SECTION" | grep -q '^_apply_effort'; then
|
||||||
|
ok "ai-eltex: starts with --effort max by default, AI_EFFORT via CLI, 64000 output tokens"
|
||||||
|
else
|
||||||
|
fail "ai-eltex: must start with --effort \"\${AI_EFFORT:-max}\", CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1, MAX_OUTPUT_TOKENS=64000, no _apply_effort"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_eltex_qwen_effort() {
|
||||||
|
if echo "$ELTEX_QWEN_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \
|
||||||
|
&& echo "$ELTEX_QWEN_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \
|
||||||
|
&& ! echo "$ELTEX_QWEN_SECTION" | grep -q '^_apply_effort'; then
|
||||||
|
ok "ai-eltex-qwen: starts with --effort max by default, 64000 output tokens"
|
||||||
|
else
|
||||||
|
fail "ai-eltex-qwen: must start with --effort \"\${AI_EFFORT:-max}\" and MAX_OUTPUT_TOKENS=64000, no _apply_effort"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-eltex: команда /logout-eltex зарегистрирована (иначе Unknown command, хук не зовётся) ──
|
||||||
|
test_eltex_logout_command() {
|
||||||
|
if echo "$ELTEX_SECTION" | grep -q 'cat > "$CLAUDE_CONFIG_DIR/commands/logout-eltex.md"'; then
|
||||||
|
ok "ai-eltex: registers /logout-eltex command so the logout hook receives it"
|
||||||
|
else
|
||||||
|
fail "ai-eltex: must create \$CLAUDE_CONFIG_DIR/commands/logout-eltex.md"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── logout hook: /logout-eltex удаляет eltex_key ──────────────────────────
|
||||||
|
test_logout_eltex() {
|
||||||
|
local tmp_home rc=0
|
||||||
|
tmp_home=$(mktemp -d)
|
||||||
|
mkdir -p "$tmp_home/.config/ai-setup"
|
||||||
|
echo "sk-test" > "$tmp_home/.config/ai-setup/eltex_key"
|
||||||
|
echo '{"prompt":"/logout"}' | HOME="$tmp_home" AI_LAUNCHER=eltex bash "$LOGOUT_HOOK" 2>/dev/null || rc=$?
|
||||||
|
if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/eltex_key" ]; then
|
||||||
|
ok "logout hook: /logout in ai-eltex removes eltex_key"
|
||||||
|
else
|
||||||
|
fail "logout hook: /logout in ai-eltex must remove eltex_key (rc=$rc)"
|
||||||
|
fi
|
||||||
|
rm -rf "$tmp_home"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-mimo: Claude Code через Xiaomi MiMo V2.6 ───────────────────────────
|
||||||
|
test_mimo_launcher() {
|
||||||
|
if echo "$MIMO_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
|
||||||
|
&& echo "$MIMO_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/mimo_key"' \
|
||||||
|
&& echo "$MIMO_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/mimo"' \
|
||||||
|
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_BASE_URL="$mimo_base_url"' \
|
||||||
|
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_OPUS_MODEL=mimo-v2.6-pro' \
|
||||||
|
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_SONNET_MODEL=mimo-v2.6-pro' \
|
||||||
|
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_HAIKU_MODEL=mimo-v2.6-pro' \
|
||||||
|
&& echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_SUBAGENT_MODEL=mimo-v2.6-pro' \
|
||||||
|
&& echo "$MIMO_SECTION" | grep -q -- 'claude --dangerously-skip-permissions --model opus ' \
|
||||||
|
&& echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000' \
|
||||||
|
&& echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \
|
||||||
|
&& ! echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=' \
|
||||||
|
&& echo "$MIMO_SECTION" | grep -q 'cat > "$CLAUDE_CONFIG_DIR/commands/logout-mimo.md"'; then
|
||||||
|
ok "ai-mimo: own key/config, Pro in every slot and subagents, --model opus, 128000 output, 1M window, /logout-mimo"
|
||||||
|
else
|
||||||
|
fail "ai-mimo: must use mimo_key, cfg/mimo, mimo-v2.6-pro everywhere, --model opus, 128000 output, 1M window and logout-mimo.md"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-mimo: эндпоинт региона Token Plan находится перебором и запоминается ──
|
||||||
|
test_mimo_region_probe() {
|
||||||
|
local tmp ok_run=1
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home"
|
||||||
|
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
|
||||||
|
echo "$MIMO_SECTION" | BIN_DIR="$tmp/bin" bash
|
||||||
|
# Ключ подходит только к региону ams, sgp недоступен по сети (как curl: -w печатает
|
||||||
|
# 000 и exit 28), остальные эндпоинты отвечают 401. MOCK_DOWN=1 - сети нет совсем.
|
||||||
|
cat > "$tmp/mock/curl" <<CURLEOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
for a in "\$@"; do
|
||||||
|
case "\$a" in
|
||||||
|
https://*) echo "\$a" >> "$tmp/curl.log" ;;
|
||||||
|
esac
|
||||||
|
case "\$a" in
|
||||||
|
https://*) [ -n "\${MOCK_DOWN:-}" ] && { printf '\n000'; exit 28; } ;;
|
||||||
|
esac
|
||||||
|
case "\$a" in
|
||||||
|
https://api.xiaomimimo.com/*) [ -n "\${MOCK_SK:-}" ] && { printf '{"error":{"message":"Insufficient account balance"}}\n402'; exit 0; } ;;
|
||||||
|
https://token-plan-*) [ -n "\${MOCK_SK:-}" ] && { printf '{"error":{"message":"invalid api key"}}\n401'; exit 0; } ;;
|
||||||
|
esac
|
||||||
|
case "\$a" in
|
||||||
|
https://token-plan-sgp.xiaomimimo.com/*) printf '\n000'; exit 28 ;;
|
||||||
|
https://token-plan-ams.xiaomimimo.com/*) printf '{"content":[]}\n200'; exit 0 ;;
|
||||||
|
https://*) printf '{"error":{"message":"invalid api key"}}\n401'; exit 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
CURLEOF
|
||||||
|
printf '#!/usr/bin/env bash\necho "$ANTHROPIC_BASE_URL" > "%s/claude.url"\n' "$tmp" > "$tmp/mock/claude"
|
||||||
|
chmod +x "$tmp/mock/curl" "$tmp/mock/claude"
|
||||||
|
_run_mimo() {
|
||||||
|
(cd "$tmp" && printf 'tp-test\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
|
||||||
|
MIMO_BASE_URL= AI_RESET_KEY= AI_EFFORT= bash "$tmp/bin/ai-mimo" >/dev/null 2>&1)
|
||||||
|
}
|
||||||
|
local ams=https://token-plan-ams.xiaomimimo.com/anthropic
|
||||||
|
_run_mimo || ok_run=0
|
||||||
|
[ "$(cat "$tmp/claude.url" 2>/dev/null)" = "$ams" ] || ok_run=0
|
||||||
|
[ "$(cat "$tmp/home/.config/ai-setup/mimo_base_url" 2>/dev/null)" = "$ams" ] || ok_run=0
|
||||||
|
[ "$(cat "$tmp/home/.config/ai-setup/mimo_key" 2>/dev/null)" = "tp-test" ] || ok_run=0
|
||||||
|
# Повторный запуск: сохранённый эндпоинт, без перебора регионов.
|
||||||
|
rm -f "$tmp/curl.log" "$tmp/claude.url"
|
||||||
|
_run_mimo || ok_run=0
|
||||||
|
[ "$(cat "$tmp/curl.log" 2>/dev/null)" = "$ams/v1/messages" ] || ok_run=0
|
||||||
|
[ "$(cat "$tmp/claude.url" 2>/dev/null)" = "$ams" ] || ok_run=0
|
||||||
|
# Без сети: ключ не сохраняется и Claude Code не запускается с пустым эндпоинтом.
|
||||||
|
rm -rf "$tmp/home" "$tmp/claude.url"
|
||||||
|
MOCK_DOWN=1 _run_mimo && ok_run=0
|
||||||
|
[ ! -e "$tmp/claude.url" ] && [ ! -e "$tmp/home/.config/ai-setup/mimo_key" ] || ok_run=0
|
||||||
|
# Ключ pay-as-you-go без баланса: Token Plan отвечает 401, api.xiaomimimo.com - 402,
|
||||||
|
# нужна подсказка про ключ tp-...
|
||||||
|
rm -rf "$tmp/home"
|
||||||
|
local out
|
||||||
|
out=$(cd "$tmp" && printf 'sk-test\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" MOCK_SK=1 \
|
||||||
|
MIMO_BASE_URL= AI_RESET_KEY= AI_EFFORT= bash "$tmp/bin/ai-mimo" 2>&1) && ok_run=0
|
||||||
|
echo "$out" | grep -q 'Dedicated API Key' || ok_run=0
|
||||||
|
[ ! -e "$tmp/claude.url" ] && [ ! -e "$tmp/home/.config/ai-setup/mimo_key" ] || ok_run=0
|
||||||
|
if [ "$ok_run" -eq 1 ]; then
|
||||||
|
ok "ai-mimo: finds the Token Plan region by probing (skips unreachable), saves and reuses it"
|
||||||
|
else
|
||||||
|
fail "ai-mimo: region probe must skip unreachable regions, pick the one that accepts the key and reuse it"
|
||||||
|
fi
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-mimo: effort max по умолчанию через CLI (MiMo effort игнорирует) ────
|
||||||
|
test_mimo_effort() {
|
||||||
|
if echo "$MIMO_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \
|
||||||
|
&& ! echo "$MIMO_SECTION" | grep -q '^_apply_effort'; then
|
||||||
|
ok "ai-mimo: starts with --effort max by default, AI_EFFORT via CLI"
|
||||||
|
else
|
||||||
|
fail "ai-mimo: must start with --effort \"\${AI_EFFORT:-max}\", no _apply_effort"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_logout_mimo() {
|
||||||
|
local tmp_home rc=0
|
||||||
|
tmp_home=$(mktemp -d)
|
||||||
|
mkdir -p "$tmp_home/.config/ai-setup"
|
||||||
|
echo "tp-test" > "$tmp_home/.config/ai-setup/mimo_key"
|
||||||
|
echo "https://token-plan-ams.xiaomimimo.com/anthropic" > "$tmp_home/.config/ai-setup/mimo_base_url"
|
||||||
|
echo '{"prompt":"/logout-mimo"}' | HOME="$tmp_home" AI_LAUNCHER=claude bash "$LOGOUT_HOOK" 2>/dev/null || rc=$?
|
||||||
|
if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/mimo_key" ] && [ ! -f "$tmp_home/.config/ai-setup/mimo_base_url" ]; then
|
||||||
|
ok "logout hook: /logout-mimo removes mimo_key and mimo_base_url"
|
||||||
|
else
|
||||||
|
fail "logout hook: /logout-mimo must remove mimo_key and mimo_base_url (rc=$rc)"
|
||||||
|
fi
|
||||||
|
rm -rf "$tmp_home"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-qwen: Claude Code через Alibaba Token Plan, ключ вшит ───────────────
|
||||||
|
test_qwen_launcher() {
|
||||||
|
if echo "$QWEN_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/qwen"' \
|
||||||
|
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_BASE_URL="$qwen_base_url"' \
|
||||||
|
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3.8-max' \
|
||||||
|
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3.8-max' \
|
||||||
|
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3.8-flash' \
|
||||||
|
&& echo "$QWEN_SECTION" | grep -q '^CLAUDE_CODE_SUBAGENT_MODEL=qwen3.8-max' \
|
||||||
|
&& echo "$QWEN_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \
|
||||||
|
&& echo "$QWEN_SECTION" | grep -q -- 'claude --dangerously-skip-permissions --model opus --effort "${AI_EFFORT:-max}"'; then
|
||||||
|
ok "ai-qwen: own config, qwen3.8-max in opus/sonnet/subagents, flash in haiku, 1M window, effort max"
|
||||||
|
else
|
||||||
|
fail "ai-qwen: must use cfg/qwen, qwen3.8-max/flash slots, 1M window, --model opus and --effort max"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-qwen: стартует сразу со вшитым ключом, ничего не спрашивая ─────────
|
||||||
|
test_qwen_starts_without_prompt() {
|
||||||
|
local tmp url token ok_run=1
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home"
|
||||||
|
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
|
||||||
|
echo "$QWEN_SECTION" | BIN_DIR="$tmp/bin" bash
|
||||||
|
printf '#!/usr/bin/env bash\necho "$ANTHROPIC_BASE_URL $ANTHROPIC_AUTH_TOKEN" > "%s/claude.env"\n' "$tmp" > "$tmp/mock/claude"
|
||||||
|
chmod +x "$tmp/mock/claude"
|
||||||
|
(cd "$tmp" && HOME="$tmp/home" PATH="$tmp/mock:$PATH" AI_EFFORT= bash "$tmp/bin/ai-qwen" </dev/null >/dev/null 2>&1) || ok_run=0
|
||||||
|
read -r url token < "$tmp/claude.env" 2>/dev/null || ok_run=0
|
||||||
|
[ "${url:-}" = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic" ] || ok_run=0
|
||||||
|
case "${token:-}" in sk-sp-?*) ;; *) ok_run=0 ;; esac
|
||||||
|
if [ "$ok_run" -eq 1 ]; then
|
||||||
|
ok "ai-qwen: starts Claude Code with the built-in endpoint and key, no prompt"
|
||||||
|
else
|
||||||
|
fail "ai-qwen: must start Claude Code right away with the built-in Token Plan endpoint and sk-sp- key"
|
||||||
|
fi
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ai-glm: default effort is xhigh (still overridable via AI_EFFORT) ──────
|
||||||
|
test_glm_default_effort() {
|
||||||
|
if echo "$GLM_SECTION" | grep -q -- '--effort "${AI_EFFORT:-xhigh}"' \
|
||||||
|
&& echo "$GLM_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \
|
||||||
|
&& echo "$GLM_SECTION" | grep -q 'CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1'; then
|
||||||
|
ok "ai-glm: starts with --effort xhigh by default, effort sent to Z.ai, 64000 output tokens"
|
||||||
|
else
|
||||||
|
fail "ai-glm: must start with --effort \"\${AI_EFFORT:-xhigh}\", CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 and MAX_OUTPUT_TOKENS=64000"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ── ai-gemini: native launcher generation ─────────────────────────────────
|
# ── ai-gemini: native launcher generation ─────────────────────────────────
|
||||||
test_gemini_native_launcher() {
|
test_gemini_native_launcher() {
|
||||||
if echo "$GEMINI_SECTION" | grep -q 'antigravity CLI (agy)' \
|
if echo "$GEMINI_SECTION" | grep -q 'antigravity CLI (agy)' \
|
||||||
@@ -301,6 +677,25 @@ test_kimi_official_api
|
|||||||
test_kimi_no_artemox
|
test_kimi_no_artemox
|
||||||
test_glm_claude_launcher
|
test_glm_claude_launcher
|
||||||
test_glm_official_api
|
test_glm_official_api
|
||||||
|
test_glm_default_effort
|
||||||
|
test_eltex_claude_launcher
|
||||||
|
test_eltex_model_from_key
|
||||||
|
test_eltex_three_slots
|
||||||
|
test_eltex_autologout_on_missing_model
|
||||||
|
test_eltex_no_tool_search
|
||||||
|
test_eltex_context_window
|
||||||
|
test_eltex_logout_command
|
||||||
|
test_eltex_effort
|
||||||
|
test_eltex_qwen_effort
|
||||||
|
test_logout_eltex
|
||||||
|
test_eltex_qwen_launcher
|
||||||
|
test_logout_eltex_qwen
|
||||||
|
test_mimo_launcher
|
||||||
|
test_mimo_region_probe
|
||||||
|
test_mimo_effort
|
||||||
|
test_logout_mimo
|
||||||
|
test_qwen_launcher
|
||||||
|
test_qwen_starts_without_prompt
|
||||||
test_gemini_native_launcher
|
test_gemini_native_launcher
|
||||||
test_cursor_native_launcher
|
test_cursor_native_launcher
|
||||||
test_global_rules_source_installed
|
test_global_rules_source_installed
|
||||||
|
|||||||
Reference in New Issue
Block a user