feat(opencode): Windows PowerShell installer and Desktop key file
CLI and Desktop share %USERPROFILE%\.config\opencode. Persist the API key in .arka-api-key so GUI apps do not need a login-shell export.
This commit is contained in:
1 parent
3e440b9365
commit
cb17ce8d2a
6 files changed
+531
-12
No files matched your search
@@ -10,12 +10,18 @@
|
|||||||
|-------|-----|
|
|-------|-----|
|
||||||
| [`opencode/`](opencode/) | Провайдер OpenCode **Arka-AI**: установщик, плагин, skill, каталог чат-моделей |
|
| [`opencode/`](opencode/) | Провайдер OpenCode **Arka-AI**: установщик, плагин, skill, каталог чат-моделей |
|
||||||
|
|
||||||
Однострочник OpenCode:
|
Однострочник OpenCode (macOS/Linux/WSL):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.sh | bash
|
curl -fsSL https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Windows (PowerShell, CLI и Desktop):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Public Arka AI downloads, docs, and scripts. Anonymous raw URLs, no token.
|
Public Arka AI downloads, docs, and scripts. Anonymous raw URLs, no token.
|
||||||
@@ -28,8 +34,14 @@ One top-level folder per product. No private source.
|
|||||||
|--------|------|
|
|--------|------|
|
||||||
| [`opencode/`](opencode/) | OpenCode **Arka-AI** provider: installer, plugin, skill, chat catalog |
|
| [`opencode/`](opencode/) | OpenCode **Arka-AI** provider: installer, plugin, skill, chat catalog |
|
||||||
|
|
||||||
OpenCode one-liner:
|
OpenCode one-liner (macOS/Linux/WSL):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.sh | bash
|
curl -fsSL https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Windows (PowerShell, CLI and Desktop):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex
|
||||||
|
```
|
||||||
+109
-1
@@ -56,6 +56,58 @@ curl -fsSL https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-a
|
|||||||
|
|
||||||
Пока сессия не перезапущена, разговор идёт на старой модели. Это нормально: установщик не требует Arka.
|
Пока сессия не перезапущена, разговор идёт на старой модели. Это нормально: установщик не требует Arka.
|
||||||
|
|
||||||
|
### Windows (CLI и Desktop)
|
||||||
|
|
||||||
|
Официальный каталог конфига OpenCode на Windows — **`%USERPROFILE%\.config\opencode`** (то же, что `C:\Users\<имя>\.config\opencode`). Туда же смотрит **OpenCode Desktop**. Не кладите плагин только в `%APPDATA%\opencode` — Desktop его оттуда не читает, если этого каталога ещё нет.
|
||||||
|
|
||||||
|
PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
С ключом и моделью по умолчанию:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
iwr -useb https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 `
|
||||||
|
-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:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex"
|
||||||
|
```
|
||||||
|
|
||||||
|
Git Bash / WSL — linux-скрипт, как обычно. Это **другой** OpenCode (домашний каталог WSL), не native Windows Desktop.
|
||||||
|
|
||||||
|
Скрипт пишет:
|
||||||
|
|
||||||
|
- `%USERPROFILE%\.config\opencode\plugins\arka-ai.ts`
|
||||||
|
- `%USERPROFILE%\.config\opencode\skills\arka-provider\SKILL.md`
|
||||||
|
- `%USERPROFILE%\.config\opencode\.arka-api-key` (Desktop читает ключ отсюда, login-shell не нужен)
|
||||||
|
- пользовательскую переменную среды `ARKA_API_KEY` (подхватится после полного перезапуска GUI)
|
||||||
|
- если уже есть `%APPDATA%\opencode` — копия туда же, на всякий случай
|
||||||
|
|
||||||
|
Затем **полностью закройте** OpenCode Desktop (иконка в трее тоже) и CLI, откройте снова, `/models`.
|
||||||
|
|
||||||
|
### OpenCode Desktop (все ОС)
|
||||||
|
|
||||||
|
Desktop — это окно + тот же sidecar CLI. Плагины те же файлы, что и у терминала.
|
||||||
|
|
||||||
|
| ОС | Плагин / skill / ключ |
|
||||||
|
|----|------------------------|
|
||||||
|
| Windows | `%USERPROFILE%\.config\opencode\` |
|
||||||
|
| macOS / Linux | `~/.config/opencode/` |
|
||||||
|
|
||||||
|
1. Поставьте провайдер скриптом выше (bash или `.ps1`).
|
||||||
|
2. **Quit** приложения, не только закрытие окна. На Windows проверьте трей.
|
||||||
|
3. Запустите Desktop снова. `/models` → **Arka-AI**.
|
||||||
|
|
||||||
|
`~/.bashrc` Desktop **не читает**. Поэтому установщик кладёт ключ в `.arka-api-key` и (на Windows) в User environment. Если модели есть в списке, но запросы 401 — ключ не попал в процесс: проверьте файл ключа и перезапуск.
|
||||||
|
|
||||||
### Что ставится
|
### Что ставится
|
||||||
|
|
||||||
| Куда | Что |
|
| Куда | Что |
|
||||||
@@ -168,6 +220,61 @@ What happens:
|
|||||||
|
|
||||||
Until you restart, this session keeps the old model. That is expected: the installer does not need Arka.
|
Until you restart, this session keeps the old model. That is expected: the installer does not need Arka.
|
||||||
|
|
||||||
|
### Windows (CLI and Desktop)
|
||||||
|
|
||||||
|
OpenCode’s real config dir on Windows is **`%USERPROFILE%\.config\opencode`**
|
||||||
|
(`C:\Users\<name>\.config\opencode`). **OpenCode Desktop uses that same folder.**
|
||||||
|
Putting the plugin only in `%APPDATA%\opencode` is a common miss — Desktop will not
|
||||||
|
load it unless that directory already exists and happens to be in use.
|
||||||
|
|
||||||
|
PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
With a key and default model:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
iwr -useb https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 `
|
||||||
|
-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:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex"
|
||||||
|
```
|
||||||
|
|
||||||
|
Git Bash / WSL use the Linux installer. That OpenCode is **not** the native Windows Desktop app (different home directory).
|
||||||
|
|
||||||
|
The script writes:
|
||||||
|
|
||||||
|
- `%USERPROFILE%\.config\opencode\plugins\arka-ai.ts`
|
||||||
|
- `%USERPROFILE%\.config\opencode\skills\arka-provider\SKILL.md`
|
||||||
|
- `%USERPROFILE%\.config\opencode\.arka-api-key` (Desktop reads the key here; no login shell needed)
|
||||||
|
- User environment variable `ARKA_API_KEY` (GUI apps pick this up after a full restart)
|
||||||
|
- a copy under `%APPDATA%\opencode` only if that directory already exists
|
||||||
|
|
||||||
|
Then **fully quit** OpenCode Desktop (tray icon too) and the CLI, start again, `/models`.
|
||||||
|
|
||||||
|
### OpenCode Desktop (all platforms)
|
||||||
|
|
||||||
|
Desktop is a window plus the same sidecar CLI. Plugins are the same files as the terminal.
|
||||||
|
|
||||||
|
| OS | Plugin / skill / key |
|
||||||
|
|----|----------------------|
|
||||||
|
| Windows | `%USERPROFILE%\.config\opencode\` |
|
||||||
|
| macOS / Linux | `~/.config/opencode/` |
|
||||||
|
|
||||||
|
1. Run the installer above (bash or `.ps1`).
|
||||||
|
2. **Quit** the app, not just the window. On Windows, check the tray.
|
||||||
|
3. Start Desktop again. `/models` → **Arka-AI**.
|
||||||
|
|
||||||
|
Desktop does **not** read `~/.bashrc`. That is why the installer writes `.arka-api-key` and (on Windows) a User env var. If models show up but requests return 401, the key never reached the process: check the key file and restart.
|
||||||
|
|
||||||
### What the installer writes
|
### What the installer writes
|
||||||
|
|
||||||
| Path | Role |
|
| Path | Role |
|
||||||
@@ -228,7 +335,8 @@ Inference still uses `ARKA_API_KEY` against `https://api.arka-ai.ru/v1`.
|
|||||||
|
|
||||||
| File | Role |
|
| File | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `install-arka-provider.sh` | Self-contained installer (the one-liner) |
|
| `install-arka-provider.sh` | POSIX installer (macOS/Linux/WSL/Git Bash) |
|
||||||
|
| `install-arka-provider.ps1` | Windows installer (CLI + Desktop) |
|
||||||
| `plugins/arka-ai.ts` | Plugin source (embedded in the installer) |
|
| `plugins/arka-ai.ts` | Plugin source (embedded in the installer) |
|
||||||
| `skills/arka-provider/SKILL.md` | Skill source (embedded in the installer) |
|
| `skills/arka-provider/SKILL.md` | Skill source (embedded in the installer) |
|
||||||
| `catalog.json` | Filtered chat catalog, refreshed daily at 05:00 Europe/Moscow |
|
| `catalog.json` | Filtered chat catalog, refreshed daily at 05:00 Europe/Moscow |
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
# 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<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(cacheDir(), { 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 = 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/<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.
|
||||||
|
'@
|
||||||
|
|
||||||
|
$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.'
|
||||||
@@ -56,9 +56,35 @@ const PROVIDER_ID = "Arka-AI"
|
|||||||
const API_URL = "https://api.arka-ai.ru/v1"
|
const API_URL = "https://api.arka-ai.ru/v1"
|
||||||
const CATALOG_URL =
|
const CATALOG_URL =
|
||||||
"https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/catalog.json"
|
"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
|
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.
|
// Ids that must always be exposed, even if the filter would drop them.
|
||||||
const FORCED_MODELS = [
|
const FORCED_MODELS = [
|
||||||
"DeepSeek-V4-Pro",
|
"DeepSeek-V4-Pro",
|
||||||
@@ -100,7 +126,7 @@ function loadCache(): Cache | null {
|
|||||||
|
|
||||||
function saveCache(models: ModelMap): void {
|
function saveCache(models: ModelMap): void {
|
||||||
try {
|
try {
|
||||||
mkdirSync(join(homedir(), ".cache", "opencode"), { recursive: true })
|
mkdirSync(cacheDir(), { recursive: true })
|
||||||
writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), models }))
|
writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), models }))
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -149,7 +175,7 @@ async function resolveModels(): Promise<ModelMap | null> {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const key = process.env.ARKA_API_KEY
|
const key = readApiKey()
|
||||||
const json = await fetchJson(
|
const json = await fetchJson(
|
||||||
`${API_URL}/models`,
|
`${API_URL}/models`,
|
||||||
key ? { Authorization: `Bearer ${key}` } : {},
|
key ? { Authorization: `Bearer ${key}` } : {},
|
||||||
@@ -165,6 +191,7 @@ async function resolveModels(): Promise<ModelMap | null> {
|
|||||||
|
|
||||||
export default async function ArkaAIProviderPlugin() {
|
export default async function ArkaAIProviderPlugin() {
|
||||||
const models = await resolveModels()
|
const models = await resolveModels()
|
||||||
|
const apiKey = readApiKey()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config: (cfg: any) => {
|
config: (cfg: any) => {
|
||||||
@@ -174,12 +201,18 @@ export default async function ArkaAIProviderPlugin() {
|
|||||||
// Static models from opencode.json act as a fallback; the fetched
|
// Static models from opencode.json act as a fallback; the fetched
|
||||||
// catalog is layered on top so the configured default model never
|
// catalog is layered on top so the configured default model never
|
||||||
// disappears even if the catalog drops it.
|
// 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] = {
|
cfg.provider[PROVIDER_ID] = {
|
||||||
name: "Arka-AI",
|
name: "Arka-AI",
|
||||||
api: API_URL,
|
api: API_URL,
|
||||||
env: ["ARKA_API_KEY"],
|
env: ["ARKA_API_KEY"],
|
||||||
...prev,
|
...prev,
|
||||||
models: { ...(prev.models ?? {}), ...(models ?? {}) },
|
models: { ...(prev.models ?? {}), ...(models ?? {}) },
|
||||||
|
options: {
|
||||||
|
...(prev.options ?? {}),
|
||||||
|
...(apiKey ? { apiKey } : {}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -214,7 +247,8 @@ It is wired up locally with a plugin + a static `provider` block in
|
|||||||
- Static block (`opencode.json` → `provider["Arka-AI"]`) holds
|
- Static block (`opencode.json` → `provider["Arka-AI"]`) holds
|
||||||
`name`, `api`, `env`, and a small fallback `models` map that is used when the
|
`name`, `api`, `env`, and a small fallback `models` map that is used when the
|
||||||
catalog is unreachable.
|
catalog is unreachable.
|
||||||
- Inference still uses `ARKA_API_KEY` against `https://api.arka-ai.ru/v1`.
|
- 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
|
## Refresh the model list
|
||||||
|
|
||||||
@@ -268,6 +302,10 @@ if [ -n "$KEY" ]; then
|
|||||||
echo "==> wrote ARKA_API_KEY to $RC"
|
echo "==> wrote ARKA_API_KEY to $RC"
|
||||||
fi
|
fi
|
||||||
export ARKA_API_KEY="$KEY"
|
export ARKA_API_KEY="$KEY"
|
||||||
|
KEY_FILE="$CONF_DIR/.arka-api-key"
|
||||||
|
printf '%s\n' "$KEY" > "$KEY_FILE"
|
||||||
|
chmod 600 "$KEY_FILE" 2>/dev/null || true
|
||||||
|
echo "==> wrote API key to $KEY_FILE (CLI + Desktop)"
|
||||||
else
|
else
|
||||||
echo "==> warning: no ARKA_API_KEY set; export it in your shell before starting opencode" >&2
|
echo "==> warning: no ARKA_API_KEY set; export it in your shell before starting opencode" >&2
|
||||||
fi
|
fi
|
||||||
@@ -299,6 +337,6 @@ echo " - provider plugin: $PLUGIN_DIR/arka-ai.ts"
|
|||||||
echo " - skill: $SKILL_DIR/SKILL.md"
|
echo " - skill: $SKILL_DIR/SKILL.md"
|
||||||
echo
|
echo
|
||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
echo " 1. Restart opencode (open a new shell so ARKA_API_KEY is exported)."
|
echo " 1. Fully quit and restart OpenCode (CLI or Desktop). Plugins load only at startup."
|
||||||
echo " 2. Run /models — the Arka-AI catalog (~116 chat models) loads on first start."
|
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"
|
echo " 3. Optional: refresh catalog later with: rm -f ~/.cache/opencode/arka-ai-models.json"
|
||||||
@@ -6,9 +6,35 @@ const PROVIDER_ID = "Arka-AI"
|
|||||||
const API_URL = "https://api.arka-ai.ru/v1"
|
const API_URL = "https://api.arka-ai.ru/v1"
|
||||||
const CATALOG_URL =
|
const CATALOG_URL =
|
||||||
"https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/catalog.json"
|
"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
|
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.
|
// Ids that must always be exposed, even if the filter would drop them.
|
||||||
const FORCED_MODELS = [
|
const FORCED_MODELS = [
|
||||||
"DeepSeek-V4-Pro",
|
"DeepSeek-V4-Pro",
|
||||||
@@ -50,7 +76,7 @@ function loadCache(): Cache | null {
|
|||||||
|
|
||||||
function saveCache(models: ModelMap): void {
|
function saveCache(models: ModelMap): void {
|
||||||
try {
|
try {
|
||||||
mkdirSync(join(homedir(), ".cache", "opencode"), { recursive: true })
|
mkdirSync(cacheDir(), { recursive: true })
|
||||||
writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), models }))
|
writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), models }))
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -99,7 +125,7 @@ async function resolveModels(): Promise<ModelMap | null> {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const key = process.env.ARKA_API_KEY
|
const key = readApiKey()
|
||||||
const json = await fetchJson(
|
const json = await fetchJson(
|
||||||
`${API_URL}/models`,
|
`${API_URL}/models`,
|
||||||
key ? { Authorization: `Bearer ${key}` } : {},
|
key ? { Authorization: `Bearer ${key}` } : {},
|
||||||
@@ -115,6 +141,7 @@ async function resolveModels(): Promise<ModelMap | null> {
|
|||||||
|
|
||||||
export default async function ArkaAIProviderPlugin() {
|
export default async function ArkaAIProviderPlugin() {
|
||||||
const models = await resolveModels()
|
const models = await resolveModels()
|
||||||
|
const apiKey = readApiKey()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config: (cfg: any) => {
|
config: (cfg: any) => {
|
||||||
@@ -124,12 +151,18 @@ export default async function ArkaAIProviderPlugin() {
|
|||||||
// Static models from opencode.json act as a fallback; the fetched
|
// Static models from opencode.json act as a fallback; the fetched
|
||||||
// catalog is layered on top so the configured default model never
|
// catalog is layered on top so the configured default model never
|
||||||
// disappears even if the catalog drops it.
|
// 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] = {
|
cfg.provider[PROVIDER_ID] = {
|
||||||
name: "Arka-AI",
|
name: "Arka-AI",
|
||||||
api: API_URL,
|
api: API_URL,
|
||||||
env: ["ARKA_API_KEY"],
|
env: ["ARKA_API_KEY"],
|
||||||
...prev,
|
...prev,
|
||||||
models: { ...(prev.models ?? {}), ...(models ?? {}) },
|
models: { ...(prev.models ?? {}), ...(models ?? {}) },
|
||||||
|
options: {
|
||||||
|
...(prev.options ?? {}),
|
||||||
|
...(apiKey ? { apiKey } : {}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ It is wired up locally with a plugin + a static `provider` block in
|
|||||||
- Static block (`opencode.json` → `provider["Arka-AI"]`) holds
|
- Static block (`opencode.json` → `provider["Arka-AI"]`) holds
|
||||||
`name`, `api`, `env`, and a small fallback `models` map that is used when the
|
`name`, `api`, `env`, and a small fallback `models` map that is used when the
|
||||||
catalog is unreachable.
|
catalog is unreachable.
|
||||||
- Inference still uses `ARKA_API_KEY` against `https://api.arka-ai.ru/v1`.
|
- 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
|
## Refresh the model list
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user