Compare commits

..

12 Commits

Author SHA1 Message Date
Виталий Никитенко
709e144a60 feat(eltex): glm-5.3 via the proxy's raw /anthropic path
Claude Code now talks to the raw /anthropic path of the Eltex proxy,
which forwards requests byte-for-byte to the Z.ai Anthropic endpoint,
the same one ai-glm uses directly:
- images work through Z.ai's server-side analyze_image (Read png ->
  server_tool_use analyze_image -> correct answer, same as ai-glm);
  the regular /v1/messages route converts to the OpenAI endpoint,
  where Z.ai rejects images with 400;
- effort reaches Z.ai: low has no thinking, then medium < high < max;
- prompt cache: ~99% of input is a cache read from the second turn,
  same as ai-glm.
The key check and model pick still use the proxy root /v1/models.

The model is glm-5.3 in every picker slot with [1m] for the 1M
window; the anth-glm-5.3 / zai-glm-5.3 / flash names are gone.
Effort stays max by default via CLI --effort.

Checked on 20.10 on llm-t2.eltex.loc:4000; admins are moving that
config to llm.eltex.loc:4000, which stays the default URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:19:37 +07:00
Виталий Никитенко
0502478081 feat(qwen): add ai-qwen launcher for Alibaba Token Plan (Qwen3.8)
ai-qwen runs Claude Code through Alibaba Cloud Model Studio's
Anthropic-compatible Token Plan endpoint
(token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic). The shared
key is built into the generated script, so it starts right away without
asking for a key.

qwen3.8-max (the smartest model the key has) is in opus/sonnet and for
subagents, --model opus on each start; qwen3.8-flash is in haiku for
background tasks. Measured on 20.10: both models accept up to 983616
input tokens (CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000, autocompact fires
before the API limit) and see images. Qwen validates
output_config.effort (bogus -> 400), Max shows no trend from low to
max, Flash thinks ~2x longer on max; the launcher starts on --effort max
like ai-mimo, EFFORT_MAPPING.md records the measurements.

setup.sh status, statusline brand color, README, QUICK_START and
AGENTS.md list the new launcher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:01:31 +07:00
Виталий Никитенко
41c5cf9b10 feat(eltex): three proxy models in /model, anth-glm-5.3 by default
Admins now give every key three models. ai-eltex maps them to the
picker slots:
- opus (default): anth-glm-5.3 - glm-5.3 on the Z.ai Anthropic
  endpoint: images via analyze_image, effort works, 1M window;
- sonnet: zai-glm-5.3 - glm-5.3 on the OpenAI endpoint, no images or
  effort, 200K. Two keys get it as zai-glm-5.3-2-2 (zai-glm-5.3 is 403
  for them), so the name is matched exactly or by prefix;
- haiku: zai-glm-5.3-flash - fast, sees images itself, no effort,
  200K; Claude Code also uses this slot for background tasks.
A missing zai/flash model falls back to the default one.

Model name is anth-glm-5.3 again: with anth-glm-5.3-5 the auto-logout
would drop every saved key, since no key has that name now.

Context window is per model instead of a global
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000: [1m] only on anth-glm-5.3
(Claude Code strips the suffix before the API call), 200K for the
others, otherwise a long flash/zai session would hit "Prompt exceeds
max length" instead of auto-compacting.

Checked on all 6 pilot keys (19.10): default model reads a png via
Read + analyze_image; sonnet/haiku answer with 200K windows; effort
low on anth-glm-5.3 = 68 output tokens (max ~3500).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:51:04 +07:00
Виталий Никитенко
f4a8c23af3 fix anth-glm-5.3-5 2026-10-16 17:01:17 +07:00
Виталий Никитенко
ef187a1b48 feat(glm): default effort xhigh instead of max
Z.ai maps xhigh to native high, so this trims the thinking budget for
everyday ai-glm launches; AI_EFFORT=max still forces the old behavior.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-16 06:51:52 +03:00
Виталий Никитенко
e6fe58eb37 feat(mimo): add ai-mimo launcher for Xiaomi MiMo V2.6 Pro
ai-mimo runs Claude Code through Xiaomi's Anthropic-compatible API
(Token Plan or pay-as-you-go). The key's endpoint is found by probing
token-plan-sgp, token-plan-ams and api.xiaomimimo.com (a key only works
in its own region) and is saved to mimo_base_url; MIMO_BASE_URL
overrides. Unreachable endpoints are skipped, token-plan-cn is left out
of the probe since it times out from our network, and a 402 on the
pay-as-you-go endpoint explains that a tp- Token Plan key is needed.

Always the smartest model: mimo-v2.6-pro in every slot and for
subagents, --model opus and --effort max on each start, 128000 output
tokens (Claude Code's ceiling), real 1M context window. Measured on
13.10: MiMo ignores output_config.effort (even "bogus" is accepted, no
length trend from low to max) and treats thinking adaptive like
enabled, so reasoning is always on; EFFORT_MAPPING.md records this.

/logout-mimo is registered as a command and handled by the logout hook
(removes mimo_key and mimo_base_url). setup.sh status, statusline brand
color, README, QUICK_START and AGENTS.md list the new launcher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:43:25 +07:00
Виталий Никитенко
6c507b4549 feat(eltex): add ai-eltex-qwen, effort max by default; fix model pick and /logout-eltex
ai-eltex-qwen: new launcher for the Eltex proxy on Qwen models
(Coder-Senior, then Flash-Next), own key eltex_qwen_key, config dir and
/logout-eltex-qwen hook branch, 200K window.

Effort: ai-glm and ai-eltex start on max via CLI --effort (AI_EFFORT
overrides), CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 so long GLM answers are
not cut at 32000. EFFORT_MAPPING notes the proxy passes effort for
anth-glm-5.3 since 09.10.

ai-eltex model pick: no longer falls back to the first /v1/models entry,
which twice picked something unusable (zai-glm-5.3 routed to flash, and
the access-group name ZAI-Test-Group -> 400). Without anth-glm-5.3 the
launcher exits with a clear message; ELTEX_MODEL still overrides.

/logout-eltex: Claude Code answered "Unknown command" and never called
the logout hook (the built-in /logout bypasses hooks too). ai-eltex now
registers commands/logout-eltex.md in its config dir, so the hook
receives the command and deletes eltex_key (checked end to end).

ru-bypass: resolve exception hosts via ahostsv4 first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:52:45 +07:00
Виталий Никитенко
e3a1c500fd fix(statusline): keep last ctx value while a Z.ai request is running
Z.ai (ai-glm, ai-eltex) streams message_start with input_tokens=0 and
sends the real usage only in the final message_delta, so while a
request runs Claude Code passes used_percentage=0 and ctx: dropped to
0%. Keep the last non-zero value per session_id in
~/.cache/ai-setup/ctx_last_<id> and show it instead of the zero.
/clear starts a new session id, so it still shows 0 there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:46:18 +07:00
Виталий Никитенко
17a943069a fix(eltex): use the real 1M context window like ai-glm
ctx: in ai-eltex filled up too fast: the launcher capped the window at
200K (CLAUDE_CODE_AUTO_COMPACT_WINDOW from the admins' doc plus
CLAUDE_CODE_DISABLE_1M_CONTEXT), while anth-glm-5.3 is the same glm-5.3
on the Z.ai Anthropic endpoint that ai-glm runs with a 1M window.
A 598K-token request went through the proxy fine (glm-5.3 answered
correctly from the last line). Claude Code now reports contextWindow
1000000.

The 200K cap also made Z.ai's analyze_image turns dangerous: their
usage covers both internal model passes (~x2), e.g. 82.7K -> 166.7K
(83% of 200K), which could trigger an early auto-compact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:39:32 +07:00
Виталий Никитенко
5e1e3cc796 fix(eltex): default to anth-glm-5.3, drop forced ENABLE_TOOL_SEARCH
Admins moved the pilot to anth-glm-5.3: proxy deployments on the Z.ai
Anthropic endpoint. Checked on all 6 pilot keys: 48/48 requests served
by glm-5.3 (no flash), images recognised via Z.ai analyze_image in
Claude Code 8/8. zai-glm-5.3 goes through an auto-router that swaps in
glm-5.3-flash, so ai-eltex now prefers anth-glm-5.3 when the key has it
(ELTEX_MODEL still overrides).

ENABLE_TOOL_SEARCH is no longer forced: on the anthropic path GLM
searched for Read/Bash via ToolSearch and decided they were missing.

Effort still has no effect: Claude Code sends output_config.effort, but
LiteLLM drops it (direct Z.ai low/max = 81/3712 output tokens, via the
proxy ~3-5K at any level).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:08:08 +07:00
Виталий Никитенко
083a614940 feat(eltex): add ai-eltex launcher for corporate LLM proxy
ai-eltex runs Claude Code through the Eltex LiteLLM proxy
(http://llm.eltex.loc:4000, team GLM subscription). The personal key
from admins is checked via /v1/models and stored in
~/.config/ai-setup/eltex_key; the model is taken from the key's
allowed list (ELTEX_MODEL / ELTEX_BASE_URL override). The proxy
ignores effort/thinking, so no effort forcing. /logout-eltex and
/logout in ai-eltex drop the key; setup.sh status shows it.

ru-bypass: hosts block now has an end marker and only that block is
rewritten, manual entries outside it survive; llm.eltex.loc/llm2 added
since the VPN DNS does not resolve it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:21:02 +07:00
Виталий Никитенко
08ad4bb429 fix(glm): default effort xhigh, document real GLM 5.3 effort mapping
Z.ai Anthropic endpoint reads output_config.effort and accepts all 5
Claude Code levels, but GLM 5.3 has only low/high/max natively:
medium -> low, xhigh -> high (measured, not documented by Z.ai).
ai-glm now starts on xhigh instead of max.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:49:02 +07:00
11 changed files with 1133 additions and 41 deletions

View File

@@ -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). Скрипт обязан привести все генерируемые файлы к эталонному виду, однозначно определяемому текущей конфигурацией.

View File

@@ -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` там ни на что не влияют.

View File

@@ -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-ключи.
После установки, если команды не видны: После установки, если команды не видны:

View File

@@ -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 для недоверенного кода.

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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 ""

View File

@@ -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

View File

@@ -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)

View File

@@ -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