OpenCode Zen free tier в OMP через локальный ses_-прокси — one-shot гайд с промптом для агента
翻译
OpenCode Zen free tier в OMP и в самом OpenCode — one-shot гайд
Что это даёт
Бесплатные модели OpenCode Zen (big-pickle, muse-spark-*-contributor-free, mimo-v2.5-free, nemotron-*-free, ling-3.0-flash-fin-free, deepseek-v4-flash-free) работают и в OMP, и в самом OpenCode CLI через локальный прокси, который подставляет правильный fingerprint (User-Agent + свежий x-opencode-session формата ses_ на каждое соединение).
Почему прокси, а не просто headers в models.yml
- Zen гейт проверяет
User-Agent(opencode UA) +x-opencode-sessionв форматеses_+ наличие toolsreadиbashв теле запроса. - OMP шлёт свой sessionId (UUID) в
x-opencode-session→ 403. - Если назвать провайдера
opencode-zen/opencode-go, OMP принудительно перезаписываетx-opencode-sessionна UUID — поэтому провайдер называетсяzenproxy. - Статический
ses_в models.yml работает, но один на все сессии; прокси генерирует свежий на каждое соединение. - Сам OpenCode чинится тем же прокси:
options.baseURLпровайдераopencodeвopencode.json→http://127.0.0.1:8801/v1(шаг 7 в промпте).
Промпт для агента (Claude / OMP / любой coding agent)
Скопируй блок ниже целиком и вставь агенту. Он самодостаточен — содержит весь код и конфиг.
Настрой бесплатные модели OpenCode Zen на этой Windows-машине через локальный прокси — и в OMP, и в самом OpenCode CLI. Сделай всё сам, проверь результат реальным запросом с обеих сторон.
## Контекст
Zen free tier отдаёт 403 FreeTierError, если запрос не выглядит как от официального клиента opencode. Гейт проверяет: User-Agent = `opencode/1.18.31 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14`, заголовок `x-opencode-session` в формате `ses_` + 12 hex + 14 base62, и наличие tools `read`+`bash` в теле (OMP их шлёт сам). OMP шлёт UUID вместо ses_ → 403. Решение: локальный прокси на 127.0.0.1:8801, который инжектит свежий ses_ на каждое TCP-соединение и правильный User-Agent, и custom provider `zenproxy` в OMP, указывающий на прокси.
## Шаг 1 — достань zen API key
Ключ уже есть в OMP: `~/.omp/agent/agent.db`, таблица `auth_credentials`, `provider='opencode-zen'`, `credential_type='api_key'`, поле `data` = JSON `{"key":"sk-..."}`. Извлеки:
```bash
python -c "import sqlite3,json; print(json.loads(sqlite3.connect(r'C:/Users/$USER/.omp/agent/agent.db').execute(\"select data from auth_credentials where provider='opencode-zen' and credential_type='api_key'\").fetchone()[0])['key'])"
```
(подставь реальный путь к ~/.omp). Если ключа нет — возьми его из существующего логина zen (opencode или OMP) либо попроси пользователя.
## Шаг 2 — напиши прокси `C:/tmp/oc-echo/zen_proxy.py`
```python
#!/usr/bin/env python3
import http.server, socketserver, requests, secrets, time, sys, threading, os
UPSTREAM = "https://opencode.ai/zen"
UA = "opencode/1.18.31 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14"
B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "zen_proxy.log")
def log(msg):
line = f"{time.strftime('%H:%M:%S')} {msg}\n"
try:
with open(LOG,"a",encoding="utf-8") as f: f.write(line)
except Exception: pass
if sys.stderr:
try: sys.stderr.write(line); sys.stderr.flush()
except Exception: pass
_lock = threading.Lock(); _last_ts = 0; _counter = 0
def gen_ses():
global _last_ts, _counter
with _lock:
ts = int(time.time()*1000)
if ts != _last_ts: _last_ts = ts; _counter = 0
_counter += 1
now = ts*0x1000 + _counter
tb = bytes((now >> (40-8*i)) & 0xFF for i in range(6))
return "ses_" + tb.hex() + "".join(B62[secrets.randbelow(62)] for _ in range(14))
HOP = {"connection","keep-alive","transfer-encoding","content-length","content-encoding","host","accept-encoding"}
class Handler(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"; _ses = None
def ses(self):
if self._ses is None:
self._ses = gen_ses()
log(f"[zen-proxy] new connection -> {self._ses}")
return self._ses
def _proxy(self):
length = int(self.headers.get("Content-Length") or 0)
body = self.rfile.read(length) if length else None
headers = {k:v for k,v in self.headers.items() if k.lower() not in HOP}
headers["User-Agent"] = UA
headers["x-opencode-session"] = self.ses()
headers["Accept-Encoding"] = "gzip" # requests decodes gzip natively; br/zstd would leak raw bytes
try:
r = requests.request(self.command, UPSTREAM+self.path, headers=headers, data=body, stream=True, timeout=600)
except Exception as e:
msg = f"upstream error: {e}".encode()
self.send_response(502); self.send_header("Content-Type","text/plain"); self.send_header("Content-Length",str(len(msg))); self.end_headers(); self.wfile.write(msg); return
self.send_response(r.status_code)
for k,v in r.headers.items():
if k.lower() not in HOP: self.send_header(k,v)
self.send_header("Transfer-Encoding","chunked"); self.end_headers()
try:
for chunk in r.iter_content(chunk_size=8192):
if chunk: self.wfile.write(b"%X\r\n%s\r\n" % (len(chunk),chunk)); self.wfile.flush()
self.wfile.write(b"0\r\n\r\n"); self.wfile.flush()
except (BrokenPipeError, ConnectionResetError): pass
r.close()
do_GET = do_POST = do_PUT = do_DELETE = do_PATCH = _proxy
def log_message(self,*a): pass
class Server(socketserver.ThreadingTCPServer):
daemon_threads = True; allow_reuse_address = True
def handle_error(self, request, client_address): pass # client disconnects are normal
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv)>1 else 8801
srv = Server(("127.0.0.1",port), Handler)
log(f"[zen-proxy] listening on 127.0.0.1:{port} -> {UPSTREAM}")
srv.serve_forever()
```
ВАЖНО: в каталоге `C:/tmp/oc-echo/` не должно быть файла `bisect.py` — он затеняет stdlib `bisect` и ломает `import requests`. Если есть — переименуй.
## Шаг 3 — запусти прокси
`python C:/tmp/oc-echo/zen_proxy.py 8801` в фоне (hub start / detached / start /min). Проверь: `curl http://127.0.0.1:8801/v1/models` → 200.
## Шаг 4 — добавь provider в `~/.omp/agent/models.yml`
Под ключом `providers:` добавь (подставь KEY из шага 1):
```yaml
zenproxy:
name: Zen Proxy (free tier)
apiKey: KEY
authHeader: true
baseUrl: http://127.0.0.1:8801/v1
headers:
User-Agent: opencode/1.18.31 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14
models:
- {id: big-pickle, name: Big Pickle, api: openai-completions, contextWindow: 200000, maxTokens: 32768, reasoning: true, input: [text], supportsTools: true, cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}}
- {id: deepseek-v4-flash-free, name: DeepSeek V4 Flash Free, api: openai-completions, contextWindow: 200000, maxTokens: 128000, reasoning: true, input: [text], supportsTools: true, cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}}
- {id: muse-spark-1.3-contributor-free, name: Muse Spark 1.3 Contributor Free, api: openai-responses, contextWindow: 1048576, maxTokens: 131072, reasoning: true, input: [text, image], supportsTools: true, cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}}
- {id: muse-spark-1.2-contributor-free, name: Muse Spark 1.2 Contributor Free, api: openai-responses, contextWindow: 1048576, maxTokens: 131072, reasoning: true, input: [text, image], supportsTools: true, cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}}
- {id: mimo-v2.5-free, name: MiMo V2.5 Free, api: openai-completions, contextWindow: 200000, maxTokens: 32768, reasoning: true, input: [text, image], supportsTools: true, cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}}
- {id: ling-3.0-flash-fin-free, name: Ling 3.0 Flash Fin Free, api: openai-completions, contextWindow: 262144, maxTokens: 32768, reasoning: true, input: [text], supportsTools: true, cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}}
- {id: nemotron-3-ultra-free, name: Nemotron 3 Ultra Free, api: openai-completions, contextWindow: 1048576, maxTokens: 128000, reasoning: true, input: [text], supportsTools: true, cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}}
- {id: nemotron-3.5-lightning-free, name: Nemotron 3.5 Lightning Free, api: openai-completions, contextWindow: 262144, maxTokens: 262144, reasoning: true, input: [text], supportsTools: true, cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}}
```
Имя провайдера НЕ должно быть `opencode-zen` или `opencode-go` — иначе OMP перезапишет ses_ на UUID.
## Шаг 5 — автозапуск прокси с авто-перезапуском
Прокси должен переживать падение и перезагрузку. Делаем self-healing loop + скрытый VBS-лаунчер.
Создай `C:\tmp\oc-echo\zen_proxy_loop.bat` (рестартит прокси каждые 5 сек если упал):
```bat
@echo off
:loop
pythonw C:\tmp\oc-echo\zen_proxy.py 8801 >> C:\tmp\oc-echo\zen_proxy.log 2>&1
timeout /t 5 /nobreak >nul
goto loop
```
Создай `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\zen-proxy.vbs` (запускает loop скрыто при логине, без окна):
```vbs
Set WshShell = CreateObject("WScript.Shell")
WshShell.Run "cmd /c C:\tmp\oc-echo\zen_proxy_loop.bat", 0, False
```
Запусти сейчас: `cscript //nologo "%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\zen-proxy.vbs"`. Проверь self-heal: убей процесс `pythonw`/`python` с `zen_proxy` в командной строке → через ~5 сек прокси должен снова отвечать на `curl http://127.0.0.1:8801/v1/models`.
## Шаг 6 — проверь и огороди пикер
```bash
omp models find zenproxy # должно показать 8 моделей
omp -p --model zenproxy/big-pickle "Say OK" # должен ответить OK, не 403
```
Если 403 — проверь лог прокси (он печатает `new connection -> ses_...` и `-> POST ... ses=...`). Если connection refused — прокси не запущен. Если в логе НЕТ новых строк, а 403 есть — ты выбрал НЕ тот провайдер (см. ниже).
ВАЖНО — в TUI выбирай модель строго из группы `zenproxy/`. Встроенные `opencode-zen/*` идут напрямую со своим UUID вместо ses_ и всегда дают 403 FreeTierError (доказано: тот же запрос через `zenproxy/` → 200). `opencode-go/*` — отдельный endpoint, работает напрямую, его трогать не надо.
Чтобы убрать ловушку из пикера (проверено: `omp models find` перестаёт показывать группу):
- в `~/.omp/agent/config.yml` в `enabledModels` удали строку `- opencode-zen/*` (оставь `- opencode-go/*` — он рабочий) и добавь `- zenproxy/*`;
- в тот же `config.yml` добавь жёсткий забор:
```yaml
disabledProviders:
- opencode-zen
```
## Шаг 7 — почини zen в самом OpenCode
Тот же fingerprint-гейт бьёт и официальный клиент, если его запросы не проходят проверку. Лечится тем же прокси — направь провайдера `opencode` на него.
Открой `~/.config/opencode/opencode.json` (на Windows: `%USERPROFILE%\.config\opencode\opencode.json`). В секции `provider` добавь или исправь провайдера `opencode` (остальные провайдеры не трогай):
```json
{
"provider": {
"opencode": {
"npm": "@ai-sdk/openai-compatible",
"name": "OpenCode Zen",
"options": {
"baseURL": "http://127.0.0.1:8801/v1"
},
"models": {
"big-pickle": { "name": "Big Pickle" }
}
}
}
}
```
Пояснения: `options.baseURL` переопределяет endpoint zen-запросов opencode (подтверждено исходниками: `resolveSDK` в `packages/opencode/src/provider/provider.ts` берёт `options.baseURL` вместо `model.api.url`). Остальные free-модели подтянутся из удалённого каталога автоматически — локальный блок `models` нужен только как минимум для smoke-теста. Auth не трогай: opencode сам приложит свой zen-токен из `auth.json`.
Проверь (нужен установленный `opencode` CLI и залогиненный zen):
```bash
opencode run --model opencode/big-pickle "Say OK" # должен ответить OK, не FreeTierError
```
## Готово
OMP: `zenproxy/big-pickle`, `zenproxy/muse-spark-1.3-contributor-free` (проверен только что: 200 OK), `zenproxy/muse-spark-1.2-contributor-free`, `zenproxy/mimo-v2.5-free`, `zenproxy/ling-3.0-flash-fin-free`, `zenproxy/nemotron-3.5-lightning-free`, `zenproxy/nemotron-3-ultra-free`. OpenCode CLI: `opencode/big-pickle`, `opencode/muse-spark-1.3-contributor-free` (проверен только что: 200 OK). `deepseek-v4-flash-free` может вернуть "Model is unavailable" — это provider-side, не fingerprint. Записи `opencode-go/*` с обеих сторон идут напрямую и работают без прокси.
Заметки
- Прокси должен быть запущен, иначе
connection refused(не 403) — касается и OMP, и OpenCode. - Диагностика 403: смотри лог прокси. Нет новых строк = запрос ушёл мимо прокси (выбран прямой провайдер). Есть строки с ses_ = запрос через прокси, смотри статус upstream.
ses_генерируется на TCP-соединение; параллельные соединения получают разные ses_ — валидно.- Custom-провайдер НЕ заменяет каталог: встроенные
opencode-zen/*остаются в пикере, пока их не убрать черезenabledModels+disabledProviders(шаг 6). - Чтобы добавить другие zen-модели в OMP — допиши в
zenproxy.modelsс правильнымapi/contextWindow/maxTokens(из/v1/models). В OpenCode остальные free-модели подтягиваются из каталога сами.
评论
?
参与讨论