# install-arka-provider.ps1 — OpenCode Arka-AI provider for Windows # (CLI and Desktop). Idempotent. Canonical files also live next to this script. # # One-liner (PowerShell): # irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex # # With key + default model: # iwr -useb URL -OutFile $env:TEMP\install-arka.ps1 # powershell -ExecutionPolicy Bypass -File $env:TEMP\install-arka.ps1 -Key $env:ARKA_API_KEY -SetDefault DeepSeek-V4-Pro # # cmd.exe: # powershell -NoProfile -ExecutionPolicy Bypass -Command "irm URL | iex" [CmdletBinding()] param( [string]$Key, [string]$SetDefault ) $ErrorActionPreference = 'Stop' if (-not $Key) { $Key = $env:ARKA_API_KEY } function Get-ConfigDir { if ($env:XDG_CONFIG_HOME) { return (Join-Path $env:XDG_CONFIG_HOME 'opencode') } return (Join-Path $HOME '.config\opencode') } function Get-InstallDirs { $dirs = New-Object System.Collections.Generic.List[string] $canonical = Get-ConfigDir [void]$dirs.Add($canonical) if ($env:APPDATA) { $legacy = Join-Path $env:APPDATA 'opencode' if ((Test-Path $legacy) -and ($legacy -ne $canonical)) { [void]$dirs.Add($legacy) } } return $dirs } if (-not $Key) { $entered = Read-Host 'ARKA_API_KEY (leave blank to skip)' if ($entered) { $Key = $entered } } $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 TTL_MS = 24 * 60 * 60 * 1000 function configDir(): string { const xdg = process.env.XDG_CONFIG_HOME if (xdg) return join(xdg, "opencode") return join(homedir(), ".config", "opencode") } function cacheDir(): string { const xdg = process.env.XDG_CACHE_HOME if (xdg) return join(xdg, "opencode") return join(homedir(), ".cache", "opencode") } const CACHE_FILE = join(cacheDir(), "arka-ai-models.json") const KEY_FILE = join(configDir(), ".arka-api-key") function readApiKey(): string | undefined { const fromEnv = process.env.ARKA_API_KEY?.trim() if (fromEnv) return fromEnv try { if (existsSync(KEY_FILE)) { const fromFile = readFileSync(KEY_FILE, "utf8").trim() if (fromFile) return fromFile } } catch {} return undefined } // 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 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(cacheDir(), { recursive: true }) writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), models })) } catch {} } async function fetchJson( url: string, headers: Record, timeoutMs: number, ): Promise { 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 { 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 = readApiKey() 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() const apiKey = readApiKey() 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. // apiKey from env or ~/.config/opencode/.arka-api-key so Desktop / // Windows GUI works without a login-shell export. cfg.provider[PROVIDER_ID] = { name: "Arka-AI", api: API_URL, env: ["ARKA_API_KEY"], ...prev, models: { ...(prev.models ?? {}), ...(models ?? {}) }, options: { ...(prev.options ?? {}), ...(apiKey ? { apiKey } : {}), }, } }, } } '@ $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 uses `ARKA_API_KEY` (env) or `~/.config/opencode/.arka-api-key` (so OpenCode Desktop / Windows GUI works without a login shell). ## 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/"`. 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. '@ $dirs = Get-InstallDirs foreach ($conf in $dirs) { $pluginDir = Join-Path $conf 'plugins' $skillDir = Join-Path $conf 'skills\arka-provider' New-Item -ItemType Directory -Force -Path $pluginDir | Out-Null New-Item -ItemType Directory -Force -Path $skillDir | Out-Null $pluginPath = Join-Path $pluginDir 'arka-ai.ts' $skillPath = Join-Path $skillDir 'SKILL.md' [System.IO.File]::WriteAllText($pluginPath, $plugin.TrimStart() + "`n") [System.IO.File]::WriteAllText($skillPath, $skill.TrimStart() + "`n") Write-Host "==> plugin $pluginPath" Write-Host "==> skill $skillPath" if ($Key) { $keyFile = Join-Path $conf '.arka-api-key' [System.IO.File]::WriteAllText($keyFile, $Key.Trim() + "`n") Write-Host "==> key $keyFile" } if ($SetDefault) { $confFile = Join-Path $conf 'opencode.json' Write-Host "==> default Arka-AI/$SetDefault in $confFile" $data = $null if (Test-Path $confFile) { $data = Get-Content -Raw -Encoding UTF8 $confFile | ConvertFrom-Json } if (-not $data) { $data = New-Object psobject } $data | Add-Member -NotePropertyName '$schema' -NotePropertyValue 'https://opencode.ai/config.json' -Force $data | Add-Member -NotePropertyName model -NotePropertyValue ("Arka-AI/" + $SetDefault) -Force ($data | ConvertTo-Json -Depth 40) + "`n" | Out-File -FilePath $confFile -Encoding utf8 } } if ($Key) { $env:ARKA_API_KEY = $Key try { [Environment]::SetEnvironmentVariable('ARKA_API_KEY', $Key, 'User') Write-Host '==> User environment ARKA_API_KEY (Desktop / GUI pick this up after a full restart)' } catch { Write-Warning "could not set User environment variable: $_" } } else { Write-Warning 'no ARKA_API_KEY; Desktop will not be able to call models until you set it' } Write-Host '' Write-Host 'Done.' Write-Host 'Next:' Write-Host ' 1. Fully quit OpenCode CLI *and* OpenCode Desktop (tray icon too), then start again.' Write-Host ' 2. /models — provider Arka-AI should appear (~116 chat models).' Write-Host ' 3. Refresh later: delete %USERPROFILE%\.cache\opencode\arka-ai-models.json and restart.'