refactor: move OpenCode artifacts under opencode/
arka/public is the dump for all Arka downloads. Keep a root stub so the old installer URL still works. Document in-OpenCode bash install.
This commit is contained in:
1 parent
6e0dc4d02b
commit
22938b01ca
9 files changed
+585
-490
No files matched your search
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/opencode/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/opencode/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/opencode/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"
|
||||
Reference in new issue
Block a user