feat: OpenCode Arka-AI installer and daily chat catalog
Public one-liner for opencode. Catalog is the filtered chat list from api.arka-ai.ru/v1/models, refreshed daily at 05:00 Europe/Moscow.
This commit is contained in:
commit
8a4fd6514f
8 files changed
+1175
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Arka public artifacts
|
||||||
|
|
||||||
|
Public, anonymous files for Arka AI. Private product code stays in other Gitea
|
||||||
|
repos.
|
||||||
|
|
||||||
|
## OpenCode Arka-AI provider
|
||||||
|
|
||||||
|
`arka-ai.ru` is not in models.dev. One command installs the OpenCode provider
|
||||||
|
plugin, companion skill, and (optional) API key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.arka-ai.ru/arka/public/raw/branch/main/install-arka-provider.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
With a key and a default model:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.arka-ai.ru/arka/public/raw/branch/main/install-arka-provider.sh \
|
||||||
|
| bash -s -- --key "$ARKA_API_KEY" --set-default DeepSeek-V4-Pro
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open a **new shell** and restart opencode. `/models` should list **Arka-AI**
|
||||||
|
(~116 chat models).
|
||||||
|
|
||||||
|
The installer is offline and idempotent. It does not call `api.arka-ai.ru`.
|
||||||
|
`python3` is required only with `--set-default`.
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
| Path | Role |
|
||||||
|
|------|------|
|
||||||
|
| `install-arka-provider.sh` | Self-extracting installer (the one-liner) |
|
||||||
|
| `plugins/arka-ai.ts` | Provider plugin source (embedded in the installer) |
|
||||||
|
| `skills/arka-provider/SKILL.md` | Companion skill source (embedded in the installer) |
|
||||||
|
| `catalog.json` | Filtered chat catalog, refreshed daily at **05:00 Europe/Moscow** from `https://api.arka-ai.ru/v1/models` |
|
||||||
|
|
||||||
|
Forced keep-list: `DeepSeek-V4-Pro`, `deepseek-v4-flash-0731`, `qwen3.8-max`,
|
||||||
|
`kimi-k3`, `MiniMax-M3`, `GLM-5.3`.
|
||||||
|
|
||||||
|
Inference uses `ARKA_API_KEY` against `https://api.arka-ai.ru/v1`. Listing the
|
||||||
|
catalog does not require a key.
|
||||||
|
|
||||||
|
After install:
|
||||||
|
|
||||||
|
1. New shell so `ARKA_API_KEY` is exported (GUI/desktop launches may need the
|
||||||
|
key set in that session, not only `~/.bashrc`).
|
||||||
|
2. Restart opencode, run `/models`.
|
||||||
|
3. Refresh later: `rm -f ~/.cache/opencode/arka-ai-models.json` and restart.
|
||||||
+473
@@ -0,0 +1,473 @@
|
|||||||
|
{
|
||||||
|
"updated_at": "2026-08-27T14:10:01Z",
|
||||||
|
"source": "https://api.arka-ai.ru/v1/models",
|
||||||
|
"raw_count": 216,
|
||||||
|
"chat_count": 116,
|
||||||
|
"object": "list",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "ACE-Step-v1-3.5B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Align-DS-V",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "AnimeSharp",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "AutoGLM-Phone-9B-Multilingual",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DeepSeek-Prover-V2-7B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DeepSeek-R1",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DeepSeek-R1-Distill-Qwen-1.5B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DeepSeek-R1-Distill-Qwen-14B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DeepSeek-R1-Distill-Qwen-32B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DeepSeek-R1-Distill-Qwen-7B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DeepSeek-V3",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DeepSeek-V3.2",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DeepSeek-V3_1",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DeepSeek-V3_1-Terminus",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DeepSeek-V4-Pro",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DianJin-R1-32B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Duix.Heygem",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ERNIE-4.5-Turbo",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ERNIE-4.5-Turbo-VL",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ERNIE-5.0-Thinking",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ERNIE-X1-Turbo",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Fin-R1",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-4-32B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-4-9B-0414",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-4.6",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-4.7",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-4.7-Flash",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-4_5",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-4_5-Air",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-4_5V",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-5",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-5.1",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-5.2",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GLM-5.3",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "HY-MT1.5-7B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "HappyHorse-1.0",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "HealthGPT-L14",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Hi3DGen",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "HiDream-I1-Full",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "HuatuoGPT-o1-7B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Hunyuan3D-2",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "InfiniteTalk",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "InternVL2-8B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "InternVL3-38B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "InternVL3-78B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "KAT-Dev",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Kimi-K2.5",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Kimi-K2.7-Code",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "LTX-2",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "LegalOne-8B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Lingshu-32B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "MAI-UI-8B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "MiMo-V2.5-Pro",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "MinerU2.5",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "MinerU2.5-Pro",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "MiniMax-M2.1",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "MiniMax-M2.5",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "MiniMax-M2.7",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "MiniMax-M3",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "PDF-Extract-Kit-1.0",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "QwQ-32B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen2-7B-Instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen2-VL-72B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen2.5-14B-Instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen2.5-7B-Instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen2.5-Coder-32B-Instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-0.6B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-14B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-30B-A3B-Instruct-2507",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-32B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-4B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-8B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-Coder-30B-A3B-Instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-Coder-Flash",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-Coder-Next",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-Next-80B-A3B-Instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-Next-80B-A3B-Thinking",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-VL-235B-A22B-Instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-VL-235B-A22B-Thinking",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-VL-30B-A3B-Instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-VL-32B-Instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-VL-32B-Thinking",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-VL-4B-Instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-VL-8B-Instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3-VL-8B-Thinking",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.5-122B-A10B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.5-27B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.5-9B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.5-Flash",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.5-Plus",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.6-27B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.6-35B-A3B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.6-Flash",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.6-Max",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.6-Plus",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.7-Max",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3.7-Plus",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3Guard-Gen-0.6B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3Guard-Gen-4B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Qwen3Guard-Gen-8B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SeedVR2-3B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Sinong1.0-32B",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Step-3.7-Flash",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "UVDoc",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "VajraV1",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "deepseek-coder-33B-instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "deepseek-v4-flash-0731",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gemma-4-26B-A4B-it",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "glm-4-9b-chat",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gpt-oss-120b",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "happyhorse-1.1",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "internlm3-8b-instruct",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kimi-k3",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nonescape-v0",
|
||||||
|
"object": "model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qwen3.8-max",
|
||||||
|
"object": "model"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Executable
+304
@@ -0,0 +1,304 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# install-arka-provider.sh — one-shot installer for the arka-ai.ru LLM provider
|
||||||
|
# in a fresh opencode install.
|
||||||
|
#
|
||||||
|
# It is fully self-contained: it embeds the provider plugin and the companion
|
||||||
|
# skill, writes them into ~/.config/opencode, and persists ARKA_API_KEY.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash install-arka-provider.sh # prompt for API key
|
||||||
|
# ARKA_API_KEY=xxx bash install-arka-provider.sh # key from env (no prompt)
|
||||||
|
# bash install-arka-provider.sh --key xxx --set-default DeepSeek-V4-Pro
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --key KEY ARKA_API_KEY to persist (default: $ARKA_API_KEY, else prompt)
|
||||||
|
# --set-default MODEL also set "model": "Arka-AI/<MODEL>" in opencode.json
|
||||||
|
# -h, --help show this help
|
||||||
|
#
|
||||||
|
# Idempotent: safe to run multiple times. Overwrites plugin/skill with the
|
||||||
|
# embedded versions; never clobbers an existing opencode.json (only upserts the
|
||||||
|
# requested default model). Makes no network calls.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '2,21p' "$0"
|
||||||
|
}
|
||||||
|
|
||||||
|
KEY=""
|
||||||
|
MODEL=""
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--key) KEY="$2"; shift 2 ;;
|
||||||
|
--key=*) KEY="${1#*=}"; shift ;;
|
||||||
|
--set-default) MODEL="$2"; shift 2 ;;
|
||||||
|
--set-default=*) MODEL="${1#*=}"; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) printf 'unknown option: %s\n' "$1" >&2; usage; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
CONF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
|
||||||
|
PLUGIN_DIR="$CONF_DIR/plugins"
|
||||||
|
SKILL_DIR="$CONF_DIR/skills/arka-provider"
|
||||||
|
|
||||||
|
mkdir -p "$PLUGIN_DIR" "$SKILL_DIR" "$HOME"
|
||||||
|
|
||||||
|
echo "==> writing provider plugin to $PLUGIN_DIR/arka-ai.ts"
|
||||||
|
cat > "$PLUGIN_DIR/arka-ai.ts" <<'PLUGIN'
|
||||||
|
import { homedir } from "node:os"
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
||||||
|
import { join } from "node:path"
|
||||||
|
|
||||||
|
const PROVIDER_ID = "Arka-AI"
|
||||||
|
const API_URL = "https://api.arka-ai.ru/v1"
|
||||||
|
const CATALOG_URL =
|
||||||
|
"https://git.arka-ai.ru/arka/public/raw/branch/main/catalog.json"
|
||||||
|
const CACHE_FILE = join(homedir(), ".cache", "opencode", "arka-ai-models.json")
|
||||||
|
const TTL_MS = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
// Ids that must always be exposed, even if the filter would drop them.
|
||||||
|
const FORCED_MODELS = [
|
||||||
|
"DeepSeek-V4-Pro",
|
||||||
|
"deepseek-v4-flash-0731",
|
||||||
|
"qwen3.8-max",
|
||||||
|
"kimi-k3",
|
||||||
|
"MiniMax-M3",
|
||||||
|
"GLM-5.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
// Heuristic: drop non-chat models (TTS/ASR/image/video/OCR/embeddings/rerankers…)
|
||||||
|
// from the catalog so they don't clutter the /models picker.
|
||||||
|
// Keep in sync with scripts/sync-catalog.py
|
||||||
|
const NON_CHAT = new RegExp(
|
||||||
|
[
|
||||||
|
"tts", "asr", "audio", "speech", "voice", "whisper", "sensevoice",
|
||||||
|
"image", "(^|[.-])img", "wan", "vidu", "video", "sora", "flux",
|
||||||
|
"diffus", "kolors", "cogview", "ocr", "embedding", "(^|[.-])embed",
|
||||||
|
"rerank", "clip", "moderation", "(^|[.-])filter", "detect", "classif",
|
||||||
|
"(^|[.-])location", "(^|[.-])search", "comfyui", "meme", "hello",
|
||||||
|
"(^|[.-])sam", "florence", "esrgan", "rmbg", "background", "poster",
|
||||||
|
"(^|[.-])face", "(^|[.-])lip", "translate", "(^|[.-])mt([2-]|$)",
|
||||||
|
"(^|[.-])bge", "(^|[.-])e5", "(^|[.-])jina", "(^|[.-])mpnet",
|
||||||
|
"spark-tts", "cosyvoice", "chattts", "indextts", "step-audio",
|
||||||
|
"fish-speech", "cosy", "mate-gguf", "tokenizer",
|
||||||
|
].join("|"),
|
||||||
|
"i",
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelMap = Record<string, { name: string }>
|
||||||
|
type Cache = { ts: number; models: ModelMap }
|
||||||
|
|
||||||
|
function loadCache(): Cache | null {
|
||||||
|
try {
|
||||||
|
if (existsSync(CACHE_FILE)) return JSON.parse(readFileSync(CACHE_FILE, "utf8"))
|
||||||
|
} catch {}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCache(models: ModelMap): void {
|
||||||
|
try {
|
||||||
|
mkdirSync(join(homedir(), ".cache", "opencode"), { recursive: true })
|
||||||
|
writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), models }))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJson(
|
||||||
|
url: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
timeoutMs: number,
|
||||||
|
): Promise<unknown> {
|
||||||
|
const ctrl = new AbortController()
|
||||||
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs)
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { headers, signal: ctrl.signal })
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return await res.json()
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function idsFromPayload(json: unknown): string[] {
|
||||||
|
const data = (json as { data?: Array<{ id: string }> }).data
|
||||||
|
return (data ?? []).map((m) => m.id).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toModelMap(ids: string[], filter: boolean): ModelMap {
|
||||||
|
const models: ModelMap = {}
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!filter || FORCED_MODELS.includes(id) || !NON_CHAT.test(id)) {
|
||||||
|
models[id] = { name: id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveModels(): Promise<ModelMap | null> {
|
||||||
|
const cache = loadCache()
|
||||||
|
if (cache && Date.now() - cache.ts < TTL_MS) return cache.models
|
||||||
|
|
||||||
|
try {
|
||||||
|
const models = toModelMap(idsFromPayload(await fetchJson(CATALOG_URL, {}, 5000)), false)
|
||||||
|
if (Object.keys(models).length) {
|
||||||
|
saveCache(models)
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = process.env.ARKA_API_KEY
|
||||||
|
const json = await fetchJson(
|
||||||
|
`${API_URL}/models`,
|
||||||
|
key ? { Authorization: `Bearer ${key}` } : {},
|
||||||
|
5000,
|
||||||
|
)
|
||||||
|
const models = toModelMap(idsFromPayload(json), true)
|
||||||
|
saveCache(models)
|
||||||
|
return models
|
||||||
|
} catch {
|
||||||
|
return cache?.models ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ArkaAIProviderPlugin() {
|
||||||
|
const models = await resolveModels()
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: (cfg: any) => {
|
||||||
|
cfg.provider ??= {}
|
||||||
|
const prev = cfg.provider[PROVIDER_ID] ?? {}
|
||||||
|
|
||||||
|
// Static models from opencode.json act as a fallback; the fetched
|
||||||
|
// catalog is layered on top so the configured default model never
|
||||||
|
// disappears even if the catalog drops it.
|
||||||
|
cfg.provider[PROVIDER_ID] = {
|
||||||
|
name: "Arka-AI",
|
||||||
|
api: API_URL,
|
||||||
|
env: ["ARKA_API_KEY"],
|
||||||
|
...prev,
|
||||||
|
models: { ...(prev.models ?? {}), ...(models ?? {}) },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PLUGIN
|
||||||
|
|
||||||
|
echo "==> writing companion skill to $SKILL_DIR/SKILL.md"
|
||||||
|
cat > "$SKILL_DIR/SKILL.md" <<'SKILL'
|
||||||
|
---
|
||||||
|
name: arka-provider
|
||||||
|
description: Manage the custom Arka-AI (arka-ai.ru) LLM provider in opencode config. Use when the user needs to add, refresh, or fix the arka-ai.ru provider, sync its model catalog, change the Arka-AI default model, or when arka models are missing from /models.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Arka-AI provider
|
||||||
|
|
||||||
|
arka-ai.ru is **not** in models.dev, so opencode ships no built-in config for it.
|
||||||
|
It is wired up locally with a plugin + a static `provider` block in
|
||||||
|
`~/.config/opencode/opencode.json`.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- Plugin: `~/.config/opencode/plugins/arka-ai.ts`
|
||||||
|
- On startup it loads the published chat catalog
|
||||||
|
`https://git.arka-ai.ru/arka/public/raw/branch/main/catalog.json`
|
||||||
|
(no API key; refreshed daily at 05:00 Europe/Moscow).
|
||||||
|
- If that fetch fails, it falls back to `https://api.arka-ai.ru/v1/models`
|
||||||
|
and filters out non-chat models (TTS/ASR/image/video/OCR/embedding/reranker/…)
|
||||||
|
via a heuristic regex so the `/models` picker stays clean.
|
||||||
|
- Caches the result at `~/.cache/opencode/arka-ai-models.json` (TTL 24h).
|
||||||
|
- Injects the provider into config via the `config` hook, layered over the
|
||||||
|
static block in `opencode.json`.
|
||||||
|
- Static block (`opencode.json` → `provider["Arka-AI"]`) holds
|
||||||
|
`name`, `api`, `env`, and a small fallback `models` map that is used when the
|
||||||
|
catalog is unreachable.
|
||||||
|
- Inference still uses `ARKA_API_KEY` against `https://api.arka-ai.ru/v1`.
|
||||||
|
|
||||||
|
## Refresh the model list
|
||||||
|
|
||||||
|
Catalog ~216 raw models / ~116 chat; the public snapshot updates at 05:00
|
||||||
|
Europe/Moscow. Changes land in opencode only after cache expiry + restart
|
||||||
|
(config is not hot-reloaded).
|
||||||
|
|
||||||
|
1. `rm -f ~/.cache/opencode/arka-ai-models.json`
|
||||||
|
2. Restart opencode (or run `opencode` again) — the plugin re-fetches on the
|
||||||
|
next cold start.
|
||||||
|
|
||||||
|
Optionally inspect the live API (no key required for listing):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS https://api.arka-ai.ru/v1/models | python3 -m json.tool
|
||||||
|
curl -sS https://git.arka-ai.ru/arka/public/raw/branch/main/catalog.json | python3 -m json.tool
|
||||||
|
```
|
||||||
|
|
||||||
|
## Change default model
|
||||||
|
|
||||||
|
Edit `opencode.json` → `"model": "Arka-AI/<model-id>"`. Use the IDs exactly as
|
||||||
|
returned by the catalog (e.g. `Arka-AI/DeepSeek-V4-Pro`,
|
||||||
|
`Arka-AI/qwen3.8-max`, `Arka-AI/kimi-k3`).
|
||||||
|
|
||||||
|
## Adjust filtering
|
||||||
|
|
||||||
|
Edit `NON_CHAT` (regex) or `FORCED_MODELS` in the plugin file **and** the same
|
||||||
|
lists in `scripts/sync-catalog.py` in `arka/public`, then re-run the installer
|
||||||
|
and restart. `FORCED_MODELS` keeps must-have chat models even if the heuristic
|
||||||
|
would drop them.
|
||||||
|
SKILL
|
||||||
|
|
||||||
|
# --- API key -----------------------------------------------------------
|
||||||
|
|
||||||
|
if [ -z "$KEY" ]; then
|
||||||
|
read -rsp "ARKA_API_KEY (leave blank to skip): " KEY || true
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$KEY" ]; then
|
||||||
|
RC=""
|
||||||
|
for f in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
|
||||||
|
if [ -f "$f" ]; then RC="$f"; break; fi
|
||||||
|
done
|
||||||
|
RC="${RC:-$HOME/.bashrc}"
|
||||||
|
touch "$RC"
|
||||||
|
if grep -q 'ARKA_API_KEY' "$RC"; then
|
||||||
|
echo "==> ARKA_API_KEY already present in $RC (skipped)"
|
||||||
|
else
|
||||||
|
printf '\n# opencode Arka-AI provider\nexport ARKA_API_KEY="%s"\n' "$KEY" >> "$RC"
|
||||||
|
echo "==> wrote ARKA_API_KEY to $RC"
|
||||||
|
fi
|
||||||
|
export ARKA_API_KEY="$KEY"
|
||||||
|
else
|
||||||
|
echo "==> warning: no ARKA_API_KEY set; export it in your shell before starting opencode" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- default model (optional) ------------------------------------------
|
||||||
|
|
||||||
|
if [ -n "$MODEL" ]; then
|
||||||
|
CONF="$CONF_DIR/opencode.json"
|
||||||
|
echo "==> setting default model Arka-AI/$MODEL in $CONF"
|
||||||
|
python3 - "$CONF" "Arka-AI/$MODEL" <<'PY'
|
||||||
|
import json, os, sys
|
||||||
|
|
||||||
|
path, model = sys.argv[1], sys.argv[2]
|
||||||
|
data = {}
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
data.setdefault("$schema", "https://opencode.ai/config.json")
|
||||||
|
data["model"] = model
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done."
|
||||||
|
echo " - provider plugin: $PLUGIN_DIR/arka-ai.ts"
|
||||||
|
echo " - skill: $SKILL_DIR/SKILL.md"
|
||||||
|
echo
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Restart opencode (open a new shell so ARKA_API_KEY is exported)."
|
||||||
|
echo " 2. Run /models — the Arka-AI catalog (~116 chat models) loads on first start."
|
||||||
|
echo " 3. Optional: refresh catalog later with: rm -f ~/.cache/opencode/arka-ai-models.json"
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { homedir } from "node:os"
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
||||||
|
import { join } from "node:path"
|
||||||
|
|
||||||
|
const PROVIDER_ID = "Arka-AI"
|
||||||
|
const API_URL = "https://api.arka-ai.ru/v1"
|
||||||
|
const CATALOG_URL =
|
||||||
|
"https://git.arka-ai.ru/arka/public/raw/branch/main/catalog.json"
|
||||||
|
const CACHE_FILE = join(homedir(), ".cache", "opencode", "arka-ai-models.json")
|
||||||
|
const TTL_MS = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
// Ids that must always be exposed, even if the filter would drop them.
|
||||||
|
const FORCED_MODELS = [
|
||||||
|
"DeepSeek-V4-Pro",
|
||||||
|
"deepseek-v4-flash-0731",
|
||||||
|
"qwen3.8-max",
|
||||||
|
"kimi-k3",
|
||||||
|
"MiniMax-M3",
|
||||||
|
"GLM-5.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
// Heuristic: drop non-chat models (TTS/ASR/image/video/OCR/embeddings/rerankers…)
|
||||||
|
// from the catalog so they don't clutter the /models picker.
|
||||||
|
// Keep in sync with scripts/sync-catalog.py
|
||||||
|
const NON_CHAT = new RegExp(
|
||||||
|
[
|
||||||
|
"tts", "asr", "audio", "speech", "voice", "whisper", "sensevoice",
|
||||||
|
"image", "(^|[.-])img", "wan", "vidu", "video", "sora", "flux",
|
||||||
|
"diffus", "kolors", "cogview", "ocr", "embedding", "(^|[.-])embed",
|
||||||
|
"rerank", "clip", "moderation", "(^|[.-])filter", "detect", "classif",
|
||||||
|
"(^|[.-])location", "(^|[.-])search", "comfyui", "meme", "hello",
|
||||||
|
"(^|[.-])sam", "florence", "esrgan", "rmbg", "background", "poster",
|
||||||
|
"(^|[.-])face", "(^|[.-])lip", "translate", "(^|[.-])mt([2-]|$)",
|
||||||
|
"(^|[.-])bge", "(^|[.-])e5", "(^|[.-])jina", "(^|[.-])mpnet",
|
||||||
|
"spark-tts", "cosyvoice", "chattts", "indextts", "step-audio",
|
||||||
|
"fish-speech", "cosy", "mate-gguf", "tokenizer",
|
||||||
|
].join("|"),
|
||||||
|
"i",
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelMap = Record<string, { name: string }>
|
||||||
|
type Cache = { ts: number; models: ModelMap }
|
||||||
|
|
||||||
|
function loadCache(): Cache | null {
|
||||||
|
try {
|
||||||
|
if (existsSync(CACHE_FILE)) return JSON.parse(readFileSync(CACHE_FILE, "utf8"))
|
||||||
|
} catch {}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCache(models: ModelMap): void {
|
||||||
|
try {
|
||||||
|
mkdirSync(join(homedir(), ".cache", "opencode"), { recursive: true })
|
||||||
|
writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), models }))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJson(
|
||||||
|
url: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
timeoutMs: number,
|
||||||
|
): Promise<unknown> {
|
||||||
|
const ctrl = new AbortController()
|
||||||
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs)
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { headers, signal: ctrl.signal })
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return await res.json()
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function idsFromPayload(json: unknown): string[] {
|
||||||
|
const data = (json as { data?: Array<{ id: string }> }).data
|
||||||
|
return (data ?? []).map((m) => m.id).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toModelMap(ids: string[], filter: boolean): ModelMap {
|
||||||
|
const models: ModelMap = {}
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!filter || FORCED_MODELS.includes(id) || !NON_CHAT.test(id)) {
|
||||||
|
models[id] = { name: id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveModels(): Promise<ModelMap | null> {
|
||||||
|
const cache = loadCache()
|
||||||
|
if (cache && Date.now() - cache.ts < TTL_MS) return cache.models
|
||||||
|
|
||||||
|
try {
|
||||||
|
const models = toModelMap(idsFromPayload(await fetchJson(CATALOG_URL, {}, 5000)), false)
|
||||||
|
if (Object.keys(models).length) {
|
||||||
|
saveCache(models)
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = process.env.ARKA_API_KEY
|
||||||
|
const json = await fetchJson(
|
||||||
|
`${API_URL}/models`,
|
||||||
|
key ? { Authorization: `Bearer ${key}` } : {},
|
||||||
|
5000,
|
||||||
|
)
|
||||||
|
const models = toModelMap(idsFromPayload(json), true)
|
||||||
|
saveCache(models)
|
||||||
|
return models
|
||||||
|
} catch {
|
||||||
|
return cache?.models ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ArkaAIProviderPlugin() {
|
||||||
|
const models = await resolveModels()
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: (cfg: any) => {
|
||||||
|
cfg.provider ??= {}
|
||||||
|
const prev = cfg.provider[PROVIDER_ID] ?? {}
|
||||||
|
|
||||||
|
// Static models from opencode.json act as a fallback; the fetched
|
||||||
|
// catalog is layered on top so the configured default model never
|
||||||
|
// disappears even if the catalog drops it.
|
||||||
|
cfg.provider[PROVIDER_ID] = {
|
||||||
|
name: "Arka-AI",
|
||||||
|
api: API_URL,
|
||||||
|
env: ["ARKA_API_KEY"],
|
||||||
|
...prev,
|
||||||
|
models: { ...(prev.models ?? {}), ...(models ?? {}) },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Daily catalog refresh for arka/public. Runs on core at 05:00 Europe/Moscow.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -i /root/.ssh/arka_public_catalog -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new}"
|
||||||
|
|
||||||
|
git fetch origin main
|
||||||
|
git checkout -q main
|
||||||
|
git reset --hard origin/main
|
||||||
|
|
||||||
|
python3 scripts/sync-catalog.py
|
||||||
|
|
||||||
|
if git diff --quiet -- catalog.json; then
|
||||||
|
printf '%s no catalog change\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git add catalog.json
|
||||||
|
git -c user.name='arka-catalog-bot' -c user.email='noreply@arka-ai.ru' \
|
||||||
|
commit -m "chore(catalog): refresh $(date -u +%Y-%m-%d)"
|
||||||
|
git push origin main
|
||||||
|
printf '%s catalog pushed\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
Executable
+129
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rebuild catalog.json from the public arka-ai.ru OpenAI-compatible models list.
|
||||||
|
|
||||||
|
Keep FORCED_MODELS / NON_CHAT in lockstep with plugins/arka-ai.ts.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
API_URL = "https://api.arka-ai.ru/v1/models"
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
OUT = ROOT / "catalog.json"
|
||||||
|
|
||||||
|
FORCED_MODELS = [
|
||||||
|
"DeepSeek-V4-Pro",
|
||||||
|
"deepseek-v4-flash-0731",
|
||||||
|
"qwen3.8-max",
|
||||||
|
"kimi-k3",
|
||||||
|
"MiniMax-M3",
|
||||||
|
"GLM-5.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
NON_CHAT = re.compile(
|
||||||
|
"|".join(
|
||||||
|
[
|
||||||
|
"tts",
|
||||||
|
"asr",
|
||||||
|
"audio",
|
||||||
|
"speech",
|
||||||
|
"voice",
|
||||||
|
"whisper",
|
||||||
|
"sensevoice",
|
||||||
|
"image",
|
||||||
|
r"(^|[.-])img",
|
||||||
|
"wan",
|
||||||
|
"vidu",
|
||||||
|
"video",
|
||||||
|
"sora",
|
||||||
|
"flux",
|
||||||
|
"diffus",
|
||||||
|
"kolors",
|
||||||
|
"cogview",
|
||||||
|
"ocr",
|
||||||
|
"embedding",
|
||||||
|
r"(^|[.-])embed",
|
||||||
|
"rerank",
|
||||||
|
"clip",
|
||||||
|
"moderation",
|
||||||
|
r"(^|[.-])filter",
|
||||||
|
"detect",
|
||||||
|
"classif",
|
||||||
|
r"(^|[.-])location",
|
||||||
|
r"(^|[.-])search",
|
||||||
|
"comfyui",
|
||||||
|
"meme",
|
||||||
|
"hello",
|
||||||
|
r"(^|[.-])sam",
|
||||||
|
"florence",
|
||||||
|
"esrgan",
|
||||||
|
"rmbg",
|
||||||
|
"background",
|
||||||
|
"poster",
|
||||||
|
r"(^|[.-])face",
|
||||||
|
r"(^|[.-])lip",
|
||||||
|
"translate",
|
||||||
|
r"(^|[.-])mt([2-]|$)",
|
||||||
|
r"(^|[.-])bge",
|
||||||
|
r"(^|[.-])e5",
|
||||||
|
r"(^|[.-])jina",
|
||||||
|
r"(^|[.-])mpnet",
|
||||||
|
"spark-tts",
|
||||||
|
"cosyvoice",
|
||||||
|
"chattts",
|
||||||
|
"indextts",
|
||||||
|
"step-audio",
|
||||||
|
"fish-speech",
|
||||||
|
"cosy",
|
||||||
|
"mate-gguf",
|
||||||
|
"tokenizer",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_models() -> list[dict]:
|
||||||
|
req = urllib.request.Request(API_URL, headers={"User-Agent": "arka-public-catalog-sync"})
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
payload = json.load(resp)
|
||||||
|
data = payload.get("data") or []
|
||||||
|
if not isinstance(data, list) or not data:
|
||||||
|
raise SystemExit("empty models list from api.arka-ai.ru")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def keep(item: dict) -> bool:
|
||||||
|
mid = item.get("id") or ""
|
||||||
|
return mid in FORCED_MODELS or not NON_CHAT.search(mid)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
raw = fetch_models()
|
||||||
|
chat = [item for item in raw if keep(item)]
|
||||||
|
chat.sort(key=lambda item: item.get("id") or "")
|
||||||
|
missing = [mid for mid in FORCED_MODELS if not any(item.get("id") == mid for item in chat)]
|
||||||
|
if missing:
|
||||||
|
print("warning: forced models missing from catalog:", ", ".join(missing), file=sys.stderr)
|
||||||
|
|
||||||
|
out = {
|
||||||
|
"updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
"source": API_URL,
|
||||||
|
"raw_count": len(raw),
|
||||||
|
"chat_count": len(chat),
|
||||||
|
"object": "list",
|
||||||
|
"data": [{"id": item.get("id"), "object": "model"} for item in chat],
|
||||||
|
}
|
||||||
|
text = json.dumps(out, ensure_ascii=False, indent=2) + "\n"
|
||||||
|
OUT.write_text(text, encoding="utf-8")
|
||||||
|
print(f"wrote {OUT} raw={out['raw_count']} chat={out['chat_count']}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
name: arka-provider
|
||||||
|
description: Manage the custom Arka-AI (arka-ai.ru) LLM provider in opencode config. Use when the user needs to add, refresh, or fix the arka-ai.ru provider, sync its model catalog, change the Arka-AI default model, or when arka models are missing from /models.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Arka-AI provider
|
||||||
|
|
||||||
|
arka-ai.ru is **not** in models.dev, so opencode ships no built-in config for it.
|
||||||
|
It is wired up locally with a plugin + a static `provider` block in
|
||||||
|
`~/.config/opencode/opencode.json`.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- Plugin: `~/.config/opencode/plugins/arka-ai.ts`
|
||||||
|
- On startup it loads the published chat catalog
|
||||||
|
`https://git.arka-ai.ru/arka/public/raw/branch/main/catalog.json`
|
||||||
|
(no API key; refreshed daily at 05:00 Europe/Moscow).
|
||||||
|
- If that fetch fails, it falls back to `https://api.arka-ai.ru/v1/models`
|
||||||
|
and filters out non-chat models (TTS/ASR/image/video/OCR/embedding/reranker/…)
|
||||||
|
via a heuristic regex so the `/models` picker stays clean.
|
||||||
|
- Caches the result at `~/.cache/opencode/arka-ai-models.json` (TTL 24h).
|
||||||
|
- Injects the provider into config via the `config` hook, layered over the
|
||||||
|
static block in `opencode.json`.
|
||||||
|
- Static block (`opencode.json` → `provider["Arka-AI"]`) holds
|
||||||
|
`name`, `api`, `env`, and a small fallback `models` map that is used when the
|
||||||
|
catalog is unreachable.
|
||||||
|
- Inference still uses `ARKA_API_KEY` against `https://api.arka-ai.ru/v1`.
|
||||||
|
|
||||||
|
## Refresh the model list
|
||||||
|
|
||||||
|
Catalog ~216 raw models / ~116 chat; the public snapshot updates at 05:00
|
||||||
|
Europe/Moscow. Changes land in opencode only after cache expiry + restart
|
||||||
|
(config is not hot-reloaded).
|
||||||
|
|
||||||
|
1. `rm -f ~/.cache/opencode/arka-ai-models.json`
|
||||||
|
2. Restart opencode (or run `opencode` again) — the plugin re-fetches on the
|
||||||
|
next cold start.
|
||||||
|
|
||||||
|
Optionally inspect the live API (no key required for listing):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS https://api.arka-ai.ru/v1/models | python3 -m json.tool
|
||||||
|
curl -sS https://git.arka-ai.ru/arka/public/raw/branch/main/catalog.json | python3 -m json.tool
|
||||||
|
```
|
||||||
|
|
||||||
|
## Change default model
|
||||||
|
|
||||||
|
Edit `opencode.json` → `"model": "Arka-AI/<model-id>"`. Use the IDs exactly as
|
||||||
|
returned by the catalog (e.g. `Arka-AI/DeepSeek-V4-Pro`,
|
||||||
|
`Arka-AI/qwen3.8-max`, `Arka-AI/kimi-k3`).
|
||||||
|
|
||||||
|
## Adjust filtering
|
||||||
|
|
||||||
|
Edit `NON_CHAT` (regex) or `FORCED_MODELS` in the plugin file **and** the same
|
||||||
|
lists in `scripts/sync-catalog.py` in `arka/public`, then re-run the installer
|
||||||
|
and restart. `FORCED_MODELS` keeps must-have chat models even if the heuristic
|
||||||
|
would drop them.
|
||||||
Reference in new issue
Block a user