opencode-rebalancer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/package.json +33 -0
- package/postinstall.mjs +86 -0
- package/preuninstall.mjs +34 -0
- package/skills/opencode-rebalancer/SKILL.md +305 -0
- package/skills/opencode-rebalancer/lib/config.sh +21 -0
- package/skills/opencode-rebalancer/lib/dates.sh +6 -0
- package/skills/opencode-rebalancer/scripts/apply.sh +60 -0
- package/skills/opencode-rebalancer/scripts/archive.sh +27 -0
- package/skills/opencode-rebalancer/scripts/changelog.sh +16 -0
- package/skills/opencode-rebalancer/scripts/collect-usage.sh +99 -0
- package/templates/plans.json +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 s-w-choi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# opencode-rebalancer
|
|
2
|
+
|
|
3
|
+
Model routing rebalancer skill for [OpenCode](https://opencode.ai) with [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode). Analyzes actual usage data from the OpenCode SQLite database and proposes optimized model routing across your providers (GPT, GLM, Kimi, Qwen, etc.).
|
|
4
|
+
|
|
5
|
+
> **Note**: This skill edits `oh-my-openagent.json` and requires the [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) plugin. It also respects the `no-sisyphus-gpt` hook that prevents the Sisyphus orchestrator from using GPT models (GPT-5.4+ are exempt with specialized variant support).
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
- **Usage analysis**: Queries the OpenCode SQLite DB to show per-provider and per-agent call distribution over 7d/30d windows
|
|
10
|
+
- **Plan-aware recommendations**: Stores your provider plan info persistently so you don't have to repeat it every session
|
|
11
|
+
- **Config change tracking**: Archives previous configs and maintains a changelog so you can track what changed and when
|
|
12
|
+
- **Smart routing rules**: Built-in knowledge of the `no-sisyphus-gpt` hook, agent classifications, and rate limit constraints
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
- [OpenCode](https://opencode.ai) CLI
|
|
17
|
+
- [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) plugin (provides `oh-my-openagent.json` config and agent schema)
|
|
18
|
+
- `sqlite3` — for querying usage data
|
|
19
|
+
- `python3` — for cross-platform date math in SQL queries
|
|
20
|
+
- macOS or Linux
|
|
21
|
+
|
|
22
|
+
## Files Managed
|
|
23
|
+
|
|
24
|
+
| File | Path | Action |
|
|
25
|
+
|------|------|--------|
|
|
26
|
+
| Skill (installed) | `~/.agents/skills/opencode-rebalancer/` | **Created** by postinstall |
|
|
27
|
+
| Skill (symlink) | `~/.claude/skills/opencode-rebalancer` | **Symlinked** by postinstall |
|
|
28
|
+
| Agent config | `~/.config/opencode/oh-my-openagent.json` | **Modified** — model routing |
|
|
29
|
+
| Plan info | `~/.config/opencode/rebalancer/plans.json` | **Created** — your provider plans |
|
|
30
|
+
| Changelog | `~/.config/opencode/rebalancer/changelog.md` | **Created** — change history |
|
|
31
|
+
| Config backups | `~/.config/opencode/rebalancer/archive/*.json` | **Created** — timestamped snapshots |
|
|
32
|
+
| Usage DB | `~/.local/share/opencode/opencode.db` | **Read-only** — SQLite usage data (respects `XDG_DATA_HOME`) |
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
### From npm
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install opencode-rebalancer
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The `postinstall` script automatically:
|
|
43
|
+
1. Copies the skill to `~/.agents/skills/opencode-rebalancer/`
|
|
44
|
+
2. Symlinks it into `~/.claude/skills/` for broad agent compatibility
|
|
45
|
+
3. Removes any old `model-rebalancer` skill (if upgrading from a previous name)
|
|
46
|
+
4. Initializes `~/.config/opencode/rebalancer/` with `plans.json` and `changelog.md`
|
|
47
|
+
|
|
48
|
+
### From source
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/s-w-choi/opencode-rebalancer.git
|
|
52
|
+
cd opencode-rebalancer
|
|
53
|
+
node postinstall.mjs
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### First-time setup
|
|
57
|
+
|
|
58
|
+
On first run, the skill will ask for your provider plan info:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
💰 Provider Plan info is needed:
|
|
62
|
+
1. Your plan name and cost for each provider
|
|
63
|
+
2. Usage limits (per 5h / weekly / monthly)
|
|
64
|
+
3. Any agent-specific preferences
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This is saved to `plans.json` and never asked again (unless you set `needs_update: true`).
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
Just ask your agent:
|
|
72
|
+
|
|
73
|
+
- "rebalance models"
|
|
74
|
+
- "check current model usage"
|
|
75
|
+
- "optimize provider distribution"
|
|
76
|
+
- "I'm hitting rate limits on [provider]"
|
|
77
|
+
|
|
78
|
+
The skill automatically:
|
|
79
|
+
|
|
80
|
+
1. Reads your plan info from `plans.json`
|
|
81
|
+
2. Queries the SQLite DB for 7d/30d usage data
|
|
82
|
+
3. Cross-references usage against plan limits
|
|
83
|
+
4. Proposes changes as a diff
|
|
84
|
+
5. On approval: archives current config, applies changes, updates changelog
|
|
85
|
+
|
|
86
|
+
## Routing Strategy
|
|
87
|
+
|
|
88
|
+
The default strategy follows these principles:
|
|
89
|
+
|
|
90
|
+
1. **Sisyphus = non-GPT** — The `no-sisyphus-gpt` hook from oh-my-opencode blocks GPT for the orchestrator. GPT-5.4+ are exempt (they have specialized variant support). Use GLM/Kimi for Sisyphus.
|
|
91
|
+
2. **Reasoning agents = top-tier** — oracle, prometheus, metis, momus use the best available model (low frequency justifies cost).
|
|
92
|
+
3. **Coding agents = codex** — hephaestus, sisyphus-junior, build use codex variants.
|
|
93
|
+
4. **Lightweight = cheapest** — explore, librarian, atlas, quick, writing use the cheapest available model.
|
|
94
|
+
5. **Conserve rate limits** — High-frequency agents (junior) use slightly lower model tiers to stay within message limits.
|
|
95
|
+
|
|
96
|
+
### The `no-sisyphus-gpt` Hook
|
|
97
|
+
|
|
98
|
+
oh-my-opencode ships a built-in hook that:
|
|
99
|
+
|
|
100
|
+
1. Detects when Sisyphus uses a GPT model
|
|
101
|
+
2. For GPT-5.4+: Sets a specialized variant instead of blocking (these have native Sisyphus support)
|
|
102
|
+
3. For all other GPT models: Shows error toast and force-redirects to Hephaestus
|
|
103
|
+
|
|
104
|
+
This rebalancer respects that constraint.
|
|
105
|
+
|
|
106
|
+
## Example Configuration
|
|
107
|
+
|
|
108
|
+
A best-practice setup using multiple providers (adjust model IDs to what `opencode models` lists for you):
|
|
109
|
+
|
|
110
|
+
### Agent Routing
|
|
111
|
+
|
|
112
|
+
| Role | Agent | Primary | Variant | Fallback 1 | Fallback 2 |
|
|
113
|
+
|------|-------|---------|---------|-------------|-------------|
|
|
114
|
+
| Orchestrator | `sisyphus` | `zai-coding-plan/glm-5.1` | — | `opencode-go/kimi-k2.6` | `opencode-go/kimi-k2.5` |
|
|
115
|
+
| Reasoning | `oracle` | `openai/gpt-5.5` | `high` | `openai/gpt-5.4` | `opencode-go/glm-5.1` |
|
|
116
|
+
| Reasoning | `prometheus` | `openai/gpt-5.5` | `high` | `openai/gpt-5.4` | `opencode-go/glm-5.1` |
|
|
117
|
+
| Reasoning | `metis` | `openai/gpt-5.5` | `high` | `openai/gpt-5.4` | `opencode-go/glm-5.1` |
|
|
118
|
+
| Reasoning | `momus` | `openai/gpt-5.5` | `high` | `openai/gpt-5.4` | `opencode-go/glm-5.1` |
|
|
119
|
+
| Reasoning | `plan` | `openai/gpt-5.5` | `high` | `openai/gpt-5.4` | `opencode-go/glm-5.1` |
|
|
120
|
+
| Coding | `hephaestus` | `openai/gpt-5.3-codex` | — | `openai/gpt-5.2-codex` | `openai/gpt-5.1-codex-max` |
|
|
121
|
+
| Coding | `sisyphus-junior` | `openai/gpt-5.2-codex` | — | `openai/gpt-5.3-codex` | `openai/gpt-5.1-codex-max` |
|
|
122
|
+
| Coding | `build` | `openai/gpt-5.2-codex` | — | `openai/gpt-5.3-codex` | `openai/gpt-5.1-codex-max` |
|
|
123
|
+
| Lightweight | `explore` | `opencode-go/qwen3.6-plus` | — | `opencode-go/qwen3.5-plus` | `opencode-go/minimax-m2.7` |
|
|
124
|
+
| Lightweight | `librarian` | `opencode-go/qwen3.6-plus` | — | `opencode-go/qwen3.5-plus` | `opencode-go/minimax-m2.7` |
|
|
125
|
+
| Lightweight | `atlas` | `opencode-go/qwen3.6-plus` | — | `opencode-go/qwen3.5-plus` | `opencode-go/minimax-m2.7` |
|
|
126
|
+
| Lightweight | `OpenCode-Builder` | `opencode-go/qwen3.6-plus` | — | `opencode-go/qwen3.5-plus` | `opencode-go/minimax-m2.7` |
|
|
127
|
+
| Vision | `multimodal-looker` | `openai/gpt-5.2` | — | `openai/gpt-5.4` | `opencode/gpt-5-nano` |
|
|
128
|
+
|
|
129
|
+
### Category Routing
|
|
130
|
+
|
|
131
|
+
| Category | Primary | Variant | Fallback 1 | Fallback 2 |
|
|
132
|
+
|----------|---------|---------|-------------|-------------|
|
|
133
|
+
| `ultrabrain` | `openai/gpt-5.5` | `xhigh` | `openai/gpt-5.4` | `opencode-go/glm-5.1` |
|
|
134
|
+
| `deep` | `openai/gpt-5.5` | `medium` | `openai/gpt-5.4` | `opencode-go/glm-5.1` |
|
|
135
|
+
| `visual-engineering` | `openai/gpt-5.2` | — | `opencode-go/minimax-m2.7` | `openai/gpt-5.4` |
|
|
136
|
+
| `artistry` | `openai/gpt-5.2` | — | `opencode-go/minimax-m2.7` | `openai/gpt-5.4` |
|
|
137
|
+
| `unspecified-high` | `openai/gpt-5.2` | — | `opencode-go/minimax-m2.7` | `openai/gpt-5.4` |
|
|
138
|
+
| `quick` | `opencode-go/qwen3.6-plus` | — | `opencode-go/qwen3.5-plus` | `opencode-go/minimax-m2.7` |
|
|
139
|
+
| `unspecified-low` | `opencode-go/qwen3.6-plus` | — | `opencode-go/qwen3.5-plus` | `opencode-go/minimax-m2.7` |
|
|
140
|
+
| `writing` | `opencode-go/qwen3.6-plus` | — | `opencode-go/qwen3.5-plus` | `opencode-go/minimax-m2.7` |
|
|
141
|
+
|
|
142
|
+
### Provider Mix Rationale
|
|
143
|
+
|
|
144
|
+
| Provider | Models | Best For | Cost |
|
|
145
|
+
|----------|--------|----------|------|
|
|
146
|
+
| **ZAI** (GLM) | `glm-5.1` | Sisyphus orchestrator — no weekly cap, generous | $9/mo |
|
|
147
|
+
| **OpenCode Go** | `qwen3.6-plus`, `minimax-m2.7`, `kimi-k2.6` | Lightweight agents — cheapest per-call | $10/mo |
|
|
148
|
+
| **OpenAI** | `gpt-5.5`, `gpt-5.3-codex`, `gpt-5.2-codex` | Reasoning & coding — highest quality | $20/mo (message-limited) |
|
|
149
|
+
| **Kimi** | `k2p5`, `k2p6` | Sisyphus fallback — coding-optimized | $19/mo |
|
|
150
|
+
|
|
151
|
+
Key principle: fallback chains always cross providers so one provider outage doesn't cascade.
|
|
152
|
+
|
|
153
|
+
## Uninstall
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npm uninstall opencode-rebalancer
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The `preuninstall` script removes:
|
|
160
|
+
- `~/.agents/skills/opencode-rebalancer/` (installed skill)
|
|
161
|
+
- `~/.claude/skills/opencode-rebalancer` (symlink)
|
|
162
|
+
- `~/.config/opencode/rebalancer/` (plans, changelog, archives)
|
|
163
|
+
|
|
164
|
+
Your `oh-my-openagent.json` config is **not modified**.
|
|
165
|
+
|
|
166
|
+
## Cross-Platform
|
|
167
|
+
|
|
168
|
+
All SQLite queries use `python3` for date math instead of platform-specific `date` flags. Works on macOS and Linux. Respects `XDG_CONFIG_HOME` and `XDG_DATA_HOME` if set.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-rebalancer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Model routing rebalancer for OpenCode — analyze usage data, optimize provider distribution, track changes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"opencode",
|
|
8
|
+
"model-routing",
|
|
9
|
+
"rebalancer",
|
|
10
|
+
"provider-optimization",
|
|
11
|
+
"rate-limits"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "s-w-choi <https://github.com/s-w-choi>",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/s-w-choi/opencode-rebalancer"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"skills/",
|
|
24
|
+
"templates/",
|
|
25
|
+
"postinstall.mjs",
|
|
26
|
+
"preuninstall.mjs",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"postinstall": "node postinstall.mjs",
|
|
31
|
+
"preuninstall": "node preuninstall.mjs"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/postinstall.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { mkdirSync, cpSync, rmSync, symlinkSync, lstatSync, statSync, existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, isAbsolute } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
7
|
+
|
|
8
|
+
const xdgRaw = process.env.XDG_CONFIG_HOME;
|
|
9
|
+
const XDG = (xdgRaw && isAbsolute(xdgRaw)) ? xdgRaw : join(homedir(), '.config');
|
|
10
|
+
|
|
11
|
+
const SKILL_NAME = 'opencode-rebalancer';
|
|
12
|
+
const SKILL_SRC = join(__dirname, 'skills', SKILL_NAME);
|
|
13
|
+
const AGENTS_SKILLS_DIR = join(homedir(), '.agents', 'skills');
|
|
14
|
+
const CLAUDE_SKILLS_DIR = join(homedir(), '.claude', 'skills');
|
|
15
|
+
const SKILL_DEST = join(AGENTS_SKILLS_DIR, SKILL_NAME);
|
|
16
|
+
const CLAUDE_LINK = join(CLAUDE_SKILLS_DIR, SKILL_NAME);
|
|
17
|
+
|
|
18
|
+
const REBALANCER_DIR = join(XDG, 'opencode', 'rebalancer');
|
|
19
|
+
const ARCHIVE_DIR = join(REBALANCER_DIR, 'archive');
|
|
20
|
+
const PLANS_FILE = join(REBALANCER_DIR, 'plans.json');
|
|
21
|
+
const CHANGELOG_FILE = join(REBALANCER_DIR, 'changelog.md');
|
|
22
|
+
const TEMPLATE = join(__dirname, 'templates', 'plans.json');
|
|
23
|
+
|
|
24
|
+
const OLD_SKILL_NAME = 'model-rebalancer';
|
|
25
|
+
const OLD_SKILL_DEST = join(AGENTS_SKILLS_DIR, OLD_SKILL_NAME);
|
|
26
|
+
const OLD_CLAUDE_LINK = join(CLAUDE_SKILLS_DIR, OLD_SKILL_NAME);
|
|
27
|
+
|
|
28
|
+
const MINIMAL_PLANS = JSON.stringify(
|
|
29
|
+
{ last_updated: null, needs_update: true, providers: {}, strategy: { summary: '', rules: [] } },
|
|
30
|
+
null,
|
|
31
|
+
2
|
|
32
|
+
) + '\n';
|
|
33
|
+
|
|
34
|
+
function isSymlink(p) {
|
|
35
|
+
try { return lstatSync(p).isSymbolicLink(); } catch { return false; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// ── 1. Remove old skill (model-rebalancer → opencode-rebalancer rename) ──
|
|
40
|
+
if (existsSync(OLD_SKILL_DEST)) {
|
|
41
|
+
rmSync(OLD_SKILL_DEST, { recursive: true, force: true });
|
|
42
|
+
console.log(`opencode-rebalancer: removed old skill '${OLD_SKILL_NAME}' from ${AGENTS_SKILLS_DIR}`);
|
|
43
|
+
}
|
|
44
|
+
if (isSymlink(OLD_CLAUDE_LINK) || existsSync(OLD_CLAUDE_LINK)) {
|
|
45
|
+
rmSync(OLD_CLAUDE_LINK, { force: true });
|
|
46
|
+
console.log(`opencode-rebalancer: removed old symlink '${OLD_SKILL_NAME}' from ${CLAUDE_SKILLS_DIR}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── 2. Install skill to ~/.agents/skills/ ──
|
|
50
|
+
if (existsSync(SKILL_SRC)) {
|
|
51
|
+
mkdirSync(AGENTS_SKILLS_DIR, { recursive: true });
|
|
52
|
+
if (existsSync(SKILL_DEST)) rmSync(SKILL_DEST, { recursive: true, force: true });
|
|
53
|
+
cpSync(SKILL_SRC, SKILL_DEST, { recursive: true });
|
|
54
|
+
console.log(`opencode-rebalancer: skill installed to ${SKILL_DEST}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── 3. Symlink into ~/.claude/skills/ ──
|
|
58
|
+
if (existsSync(SKILL_DEST)) {
|
|
59
|
+
mkdirSync(CLAUDE_SKILLS_DIR, { recursive: true });
|
|
60
|
+
if (isSymlink(CLAUDE_LINK) || existsSync(CLAUDE_LINK)) rmSync(CLAUDE_LINK, { recursive: true, force: true });
|
|
61
|
+
symlinkSync(SKILL_DEST, CLAUDE_LINK);
|
|
62
|
+
console.log(`opencode-rebalancer: symlinked to ${CLAUDE_LINK}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 4. Create config directories ──
|
|
66
|
+
mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
67
|
+
|
|
68
|
+
// ── 5. Initialize plans.json if missing ──
|
|
69
|
+
if (!isSymlink(PLANS_FILE) && !statSync(PLANS_FILE, { throwIfNoEntry: false })) {
|
|
70
|
+
if (existsSync(TEMPLATE) && !isSymlink(TEMPLATE)) {
|
|
71
|
+
cpSync(TEMPLATE, PLANS_FILE);
|
|
72
|
+
} else {
|
|
73
|
+
writeFileSync(PLANS_FILE, MINIMAL_PLANS, { mode: 0o600 });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── 6. Initialize changelog.md if missing ──
|
|
78
|
+
if (!isSymlink(CHANGELOG_FILE) && !statSync(CHANGELOG_FILE, { throwIfNoEntry: false })) {
|
|
79
|
+
writeFileSync(CHANGELOG_FILE, '# Model Rebalancer Changelog\n', { mode: 0o600 });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('opencode-rebalancer: initialized');
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.warn('opencode-rebalancer: setup incomplete — ' + err.message);
|
|
85
|
+
console.warn(' Run manually: node ' + join(__dirname, 'postinstall.mjs'));
|
|
86
|
+
}
|
package/preuninstall.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { rmSync, lstatSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, isAbsolute } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const xdgRaw = process.env.XDG_CONFIG_HOME;
|
|
6
|
+
const XDG = (xdgRaw && isAbsolute(xdgRaw)) ? xdgRaw : join(homedir(), '.config');
|
|
7
|
+
|
|
8
|
+
const SKILL_NAME = 'opencode-rebalancer';
|
|
9
|
+
const AGENTS_SKILLS_DIR = join(homedir(), '.agents', 'skills');
|
|
10
|
+
const CLAUDE_SKILLS_DIR = join(homedir(), '.claude', 'skills');
|
|
11
|
+
const SKILL_DEST = join(AGENTS_SKILLS_DIR, SKILL_NAME);
|
|
12
|
+
const CLAUDE_LINK = join(CLAUDE_SKILLS_DIR, SKILL_NAME);
|
|
13
|
+
const REBALANCER_DIR = join(XDG, 'opencode', 'rebalancer');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (existsSync(SKILL_DEST)) {
|
|
17
|
+
rmSync(SKILL_DEST, { recursive: true, force: true });
|
|
18
|
+
console.log(`opencode-rebalancer: removed skill from ${SKILL_DEST}`);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const s = lstatSync(CLAUDE_LINK);
|
|
22
|
+
if (s.isSymbolicLink() || s.isDirectory()) {
|
|
23
|
+
rmSync(CLAUDE_LINK, { recursive: true, force: true });
|
|
24
|
+
console.log(`opencode-rebalancer: removed from ${CLAUDE_LINK}`);
|
|
25
|
+
}
|
|
26
|
+
} catch { /* doesn't exist */ }
|
|
27
|
+
if (existsSync(REBALANCER_DIR)) {
|
|
28
|
+
rmSync(REBALANCER_DIR, { recursive: true, force: true });
|
|
29
|
+
console.log(`opencode-rebalancer: removed config at ${REBALANCER_DIR}`);
|
|
30
|
+
}
|
|
31
|
+
console.log('opencode-rebalancer: uninstalled. Your oh-my-openagent.json was not modified.');
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.warn('opencode-rebalancer: cleanup incomplete — ' + err.message);
|
|
34
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: opencode-rebalancer
|
|
3
|
+
description: Analyzes actual OpenCode usage data from SQLite and redistributes model routing across providers to optimize cost and rate limit distribution. Use when the user asks to "rebalance models", "optimize model routing", "redistribute providers", "check model usage", or wants to adjust which models handle which agents based on real usage patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Model Rebalancer
|
|
7
|
+
|
|
8
|
+
Analyzes real usage data from the OpenCode SQLite database and proposes optimized model routing configuration across available providers (GPT, GLM, Kimi, Qwen, etc.).
|
|
9
|
+
|
|
10
|
+
**Requires**: [oh-my-openagent](https://github.com/code-yeongyu/oh-my-opencode) plugin with `no-sisyphus-gpt` hook enabled.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- User asks to rebalance/redistribute model routing
|
|
15
|
+
- User wants to check current model usage distribution
|
|
16
|
+
- User wants to optimize costs across multiple provider plans
|
|
17
|
+
- User notices rate limiting on a specific provider
|
|
18
|
+
- User wants to add a new provider/model to the rotation
|
|
19
|
+
|
|
20
|
+
## Prerequisites Check
|
|
21
|
+
|
|
22
|
+
Before proceeding, verify:
|
|
23
|
+
|
|
24
|
+
**Hard prerequisites (STOP if missing)**:
|
|
25
|
+
1. `sqlite3` CLI is available
|
|
26
|
+
2. `python3` is available
|
|
27
|
+
3. OpenCode SQLite database exists (default: `~/.local/share/opencode/opencode.db`, respects `XDG_DATA_HOME`)
|
|
28
|
+
|
|
29
|
+
**Soft prerequisites (warn but continue)**:
|
|
30
|
+
4. `opencode models` — if unavailable, skip model ID validation and warn user
|
|
31
|
+
|
|
32
|
+
## File Locations
|
|
33
|
+
|
|
34
|
+
| File | Path | Role |
|
|
35
|
+
|------|------|------|
|
|
36
|
+
| **Config** | `~/.config/opencode/oh-my-openagent.json` | Current agent/category model routing |
|
|
37
|
+
| **Plan Info** | `~/.config/opencode/rebalancer/plans.json` | User's provider plans and limits |
|
|
38
|
+
| **Change Archive** | `~/.config/opencode/rebalancer/archive/` | Timestamped config snapshots |
|
|
39
|
+
| **Changelog** | `~/.config/opencode/rebalancer/changelog.md` | Human-readable change history |
|
|
40
|
+
| **Usage DB** | `$XDG_DATA_HOME/.../opencode.db` (default `~/.local/share/opencode/opencode.db`) | SQLite with all message history |
|
|
41
|
+
| **Scripts** | (this package) `scripts/` | Executable helpers |
|
|
42
|
+
| **Lib** | (this package) `lib/` | Shared config and date helpers |
|
|
43
|
+
|
|
44
|
+
> All paths under `~/.config/` respect `XDG_CONFIG_HOME`; the DB path respects `XDG_DATA_HOME`. Scripts and lib use the same XDG-aware resolution.
|
|
45
|
+
|
|
46
|
+
## Script Resolution
|
|
47
|
+
|
|
48
|
+
This skill ships executable helpers in `scripts/` with shared config in `lib/`. When installed via npm, the postinstall script copies the skill to `~/.agents/skills/opencode-rebalancer/`. Scripts are self-locating via `SCRIPT_DIR`:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# All scripts resolve their own location and source lib/ automatically.
|
|
52
|
+
# Just invoke them directly:
|
|
53
|
+
bash ~/.agents/skills/opencode-rebalancer/scripts/collect-usage.sh
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Scripts auto-resolve all paths (XDG-aware) via `lib/config.sh`. No manual path setup needed.
|
|
57
|
+
|
|
58
|
+
## Step 0: Load Persistent Context
|
|
59
|
+
|
|
60
|
+
### 0a. Read plan info
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
REBALANCER_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/opencode/rebalancer"
|
|
64
|
+
cat "$REBALANCER_CONFIG/plans.json"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If the file exists and `needs_update` is `false`, use it directly — **DO NOT ask the user for plan info again**.
|
|
68
|
+
|
|
69
|
+
If the file does NOT exist or `needs_update: true`, ask the user:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
💰 Provider Plan info is needed to make good recommendations:
|
|
73
|
+
1. Your plan name and cost for each provider (e.g. ZAI Starter $9/mo, OpenCode Go $10/mo)
|
|
74
|
+
2. Usage limits (per 5h / weekly / monthly)
|
|
75
|
+
3. Any agent-specific preferences or constraints
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
After collecting, save to `$REBALANCER_CONFIG/plans.json` (see [Plan Info Schema](#plan-info-schema)).
|
|
79
|
+
|
|
80
|
+
### 0b. Read current config
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
cat "${XDG_CONFIG_HOME:-$HOME/.config}/opencode/oh-my-openagent.json"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Step 1: Collect Usage Data
|
|
87
|
+
|
|
88
|
+
Run the collection script:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bash ~/.agents/skills/opencode-rebalancer/scripts/collect-usage.sh
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This outputs:
|
|
95
|
+
- Provider distribution (30d & 7d)
|
|
96
|
+
- Per-agent per-provider matrix (30d)
|
|
97
|
+
- Agent usage ratio (30d)
|
|
98
|
+
- Available models (via `opencode models`)
|
|
99
|
+
|
|
100
|
+
## Step 2: Analyze Current Distribution
|
|
101
|
+
|
|
102
|
+
Cross-reference usage data with plan limits from `plans.json`.
|
|
103
|
+
|
|
104
|
+
**NOTE**: "Plan utilization" is a **call-share proxy**, not exact spend. Always present utilization as an estimate.
|
|
105
|
+
|
|
106
|
+
For each provider:
|
|
107
|
+
- **Call share** (from SQLite) vs **plan limit** = estimated utilization %
|
|
108
|
+
- **Risk level**: Low (<50%), Medium (50-80%), High (>80%), Critical (>95%)
|
|
109
|
+
|
|
110
|
+
For each agent:
|
|
111
|
+
- **Current primary model** and its call count
|
|
112
|
+
- **Whether the model tier matches task complexity**
|
|
113
|
+
- **Rate limit risk** based on call frequency
|
|
114
|
+
|
|
115
|
+
## Step 3: Apply Routing Rules
|
|
116
|
+
|
|
117
|
+
### HARD CONSTRAINT: no-sisyphus-gpt Hook
|
|
118
|
+
|
|
119
|
+
oh-my-opencode has a built-in `no-sisyphus-gpt` hook that:
|
|
120
|
+
|
|
121
|
+
1. **Detects** when Sisyphus uses a GPT model (via `model.includes("gpt")`)
|
|
122
|
+
2. **For GPT-5.4+** (`gpt-5.4`, `gpt-5.5`, `gpt-5.6`, etc.): Sets a specialized variant instead of blocking — these have native Sisyphus support
|
|
123
|
+
3. **For all other GPT models**: Shows error toast "NEVER Use Sisyphus with GPT" and force-redirects Sisyphus → Hephaestus
|
|
124
|
+
|
|
125
|
+
**Rule**: Sisyphus MUST use non-GPT models (GLM, Kimi, Qwen). GPT-5.4+ are technically exempt but should be avoided due to cost.
|
|
126
|
+
|
|
127
|
+
### Agent Classification
|
|
128
|
+
|
|
129
|
+
**ORCHESTRATOR (GLM/Kimi only — GPT blocked by runtime hook)**:
|
|
130
|
+
- `sisyphus` → `zai-coding-plan/glm-5.1` (primary)
|
|
131
|
+
- Fallback: `opencode-go/kimi-k2.6` → `kimi-for-coding/k2p6`
|
|
132
|
+
|
|
133
|
+
**REASONING (GPT-5.5, low frequency, high quality)**:
|
|
134
|
+
- `oracle`, `prometheus`, `metis`, `momus` → `openai/gpt-5.5` (high variant)
|
|
135
|
+
- `plan` → `openai/gpt-5.5` (high variant)
|
|
136
|
+
|
|
137
|
+
**CODING (GPT codex, moderate frequency)**:
|
|
138
|
+
- `hephaestus` → `openai/gpt-5.3-codex`
|
|
139
|
+
- `sisyphus-junior` → `openai/gpt-5.2-codex` (slightly lower for rate limit headroom)
|
|
140
|
+
- `build` → `openai/gpt-5.2-codex`
|
|
141
|
+
|
|
142
|
+
**LIGHTWEIGHT (OpenCode Go or equivalent — cheapest, generous limits)**:
|
|
143
|
+
- `explore`, `librarian`, `atlas` → cheapest available lightweight model
|
|
144
|
+
- `OpenCode-Builder` → cheapest available lightweight model
|
|
145
|
+
|
|
146
|
+
**SPECIAL CASES**:
|
|
147
|
+
- `multimodal-looker` → Must use vision-capable model
|
|
148
|
+
|
|
149
|
+
### Category Classification
|
|
150
|
+
|
|
151
|
+
| Category | Typical Tier | Rationale |
|
|
152
|
+
|----------|-------------|-----------|
|
|
153
|
+
| `ultrabrain` | Highest | Hardest tasks, needs top reasoning |
|
|
154
|
+
| `deep` | High | Deep autonomous work |
|
|
155
|
+
| `visual-engineering` | Medium-High | UI/UX needs capable model |
|
|
156
|
+
| `artistry` | Medium | Creative problem solving |
|
|
157
|
+
| `unspecified-high` | Medium | General high-effort tasks |
|
|
158
|
+
| `quick` | Low | Trivial tasks, save expensive model quota |
|
|
159
|
+
| `unspecified-low` | Low | Low effort tasks |
|
|
160
|
+
| `writing` | Low | Documentation |
|
|
161
|
+
|
|
162
|
+
### Full Agent List (14 agents in oh-my-openagent schema)
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
sisyphus, sisyphus-junior, hephaestus, oracle, prometheus,
|
|
166
|
+
metis, momus, explore, librarian, atlas, multimodal-looker,
|
|
167
|
+
build, plan, OpenCode-Builder
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Redistribution Principles
|
|
171
|
+
|
|
172
|
+
1. **no-sisyphus-gpt**: Sisyphus MUST NOT use GPT. GPT-5.4+ are technically exempt but costly. This is enforced by a runtime hook.
|
|
173
|
+
2. **Codex for coding**: hephaestus, sisyphus-junior, build → GPT codex variants
|
|
174
|
+
3. **Conserve expensive model rate limits**: Use top-tier models only for reasoning and coding agents.
|
|
175
|
+
4. **Reserve primary cheap provider for orchestrator**: If the user has a provider with no weekly cap (e.g. ZAI Starter), use it primarily for Sisyphus.
|
|
176
|
+
5. **Use secondary cheap provider for lightweight**: explore, librarian, atlas, writing → cheapest available.
|
|
177
|
+
6. **Fallback chains**: Always specify 2-3 fallbacks with different providers.
|
|
178
|
+
7. **Manual override**: Note that OpenCode Go includes kimi-k2.5, kimi-k2.6, glm-5.1 — user can manually switch Sisyphus to Go models when primary limits are hit.
|
|
179
|
+
|
|
180
|
+
## Step 4: Generate Configuration
|
|
181
|
+
|
|
182
|
+
Read the current config:
|
|
183
|
+
```bash
|
|
184
|
+
cat "${XDG_CONFIG_HOME:-$HOME/.config}/opencode/oh-my-openagent.json"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Generate the updated config. **Before applying, confirm with user**:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
🎯 How would you like to proceed?
|
|
191
|
+
1. **Full refresh**: Update all agents/categories at once
|
|
192
|
+
2. **Partial update**: Only change specific agents (e.g. just sisyphus)
|
|
193
|
+
3. **Accept recommendation**: Apply data-driven optimization as-is
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Present changes as a diff.
|
|
197
|
+
|
|
198
|
+
## Step 5: Apply, Archive, and Report
|
|
199
|
+
|
|
200
|
+
### 5a. Archive current config
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
bash ~/.agents/skills/opencode-rebalancer/scripts/archive.sh
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 5b. Apply changes (SAFE WRITE)
|
|
207
|
+
|
|
208
|
+
**NEVER write directly to `oh-my-openagent.json`.** Use the apply script:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
bash ~/.agents/skills/opencode-rebalancer/scripts/apply.sh /path/to/new-config.json
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The script validates JSON before replacing the original. If validation fails, the original is preserved untouched.
|
|
215
|
+
|
|
216
|
+
### 5c. Create changelog entry
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
echo "### Changes
|
|
220
|
+
- agent: model_before → model_after (reason)
|
|
221
|
+
|
|
222
|
+
### Provider Distribution
|
|
223
|
+
- provider-a: X% (Y calls/7d)
|
|
224
|
+
|
|
225
|
+
### Plan Utilization
|
|
226
|
+
- Plan A: Z% utilization" | bash ~/.agents/skills/opencode-rebalancer/scripts/changelog.sh
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 5d. Report to user
|
|
230
|
+
|
|
231
|
+
1. **Before/After table** showing each agent's model change
|
|
232
|
+
2. **Provider distribution projection** (estimated new call distribution)
|
|
233
|
+
3. **Plan utilization** (each plan's usage vs limit)
|
|
234
|
+
4. **Risk notes** (any agents where the model was changed)
|
|
235
|
+
|
|
236
|
+
## Plan Info Schema
|
|
237
|
+
|
|
238
|
+
`$XDG_CONFIG_HOME/opencode/rebalancer/plans.json` (default: `~/.config/opencode/rebalancer/plans.json`):
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"last_updated": "YYYY-MM-DD",
|
|
243
|
+
"needs_update": false,
|
|
244
|
+
"providers": {
|
|
245
|
+
"provider-id": {
|
|
246
|
+
"plan_name": "Plan Name",
|
|
247
|
+
"monthly_cost": 0,
|
|
248
|
+
"currency": "USD",
|
|
249
|
+
"limits": {
|
|
250
|
+
"5h": "description",
|
|
251
|
+
"weekly": "description",
|
|
252
|
+
"monthly": "description"
|
|
253
|
+
},
|
|
254
|
+
"notes": "Any provider-specific quirks",
|
|
255
|
+
"models": ["model-a", "model-b"]
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
"strategy": {
|
|
259
|
+
"summary": "One-line summary of the routing strategy",
|
|
260
|
+
"rules": [
|
|
261
|
+
"Rule 1",
|
|
262
|
+
"Rule 2"
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Config Schema Reference
|
|
269
|
+
|
|
270
|
+
The `oh-my-openagent.json` file structure:
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{
|
|
274
|
+
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
|
275
|
+
"agents": {
|
|
276
|
+
"<agent-name>": {
|
|
277
|
+
"model": "provider/model-id",
|
|
278
|
+
"variant": "high|medium|low|xhigh",
|
|
279
|
+
"fallback_models": ["provider/model-2", "provider/model-3"]
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
"categories": {
|
|
283
|
+
"<category-name>": {
|
|
284
|
+
"model": "provider/model-id",
|
|
285
|
+
"variant": "high|medium|low|xhigh",
|
|
286
|
+
"fallback_models": ["provider/model-2", "provider/model-3"]
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
"_migrations": ["..."]
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Important Constraints
|
|
294
|
+
|
|
295
|
+
- **NEVER** set `sisyphus` to any GPT model (the hook redirects to Hephaestus; GPT-5.4+ are technically exempt but costly)
|
|
296
|
+
- **NEVER** set `multimodal-looker` to a model without verified vision support
|
|
297
|
+
- **NEVER** use model IDs that don't exist in `opencode models` output
|
|
298
|
+
- **ALWAYS** validate fallback model IDs exist
|
|
299
|
+
- **ALWAYS** preserve the `$schema` field in config
|
|
300
|
+
- **ALWAYS** show changes as diff before applying
|
|
301
|
+
- **NEVER** remove existing agent entries (only update/add)
|
|
302
|
+
- **ALWAYS** archive current config before applying changes
|
|
303
|
+
- **ALWAYS** read `plans.json` first — don't re-ask user for plan info
|
|
304
|
+
- **ALWAYS** update `changelog.md` after applying changes
|
|
305
|
+
- **ALWAYS** use `python3` for date math (cross-platform, not `date -v` or `date -d`)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Usage: source "$(dirname "$0")/../lib/config.sh"
|
|
3
|
+
|
|
4
|
+
_CONFIG_HOME="${XDG_CONFIG_HOME:-}"
|
|
5
|
+
if [ -z "$_CONFIG_HOME" ] || [ ! -d "$_CONFIG_HOME" ]; then
|
|
6
|
+
_CONFIG_HOME="$HOME/.config"
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
_DATA_HOME="${XDG_DATA_HOME:-}"
|
|
10
|
+
if [ -z "$_DATA_HOME" ] || [ ! -d "$_DATA_HOME" ]; then
|
|
11
|
+
_DATA_HOME="$HOME/.local/share"
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
REBALANCER_DIR="$_CONFIG_HOME/opencode/rebalancer"
|
|
15
|
+
REBALANCER_ARCHIVE="$REBALANCER_DIR/archive"
|
|
16
|
+
REBALANCER_PLANS="$REBALANCER_DIR/plans.json"
|
|
17
|
+
REBALANCER_CHANGELOG="$REBALANCER_DIR/changelog.md"
|
|
18
|
+
OPENAGENT_CONFIG="$_CONFIG_HOME/opencode/oh-my-openagent.json"
|
|
19
|
+
OPENCODE_DB="$_DATA_HOME/opencode/opencode.db"
|
|
20
|
+
|
|
21
|
+
unset _CONFIG_HOME _DATA_HOME
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
4
|
+
source "$SCRIPT_DIR/../lib/config.sh"
|
|
5
|
+
|
|
6
|
+
if [ $# -lt 1 ]; then
|
|
7
|
+
echo "Usage: apply.sh <new-config.json>" >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
NEW_CONFIG="$1"
|
|
12
|
+
TMP="${OPENAGENT_CONFIG}.tmp"
|
|
13
|
+
|
|
14
|
+
if [ ! -f "$NEW_CONFIG" ]; then
|
|
15
|
+
echo "ERROR: File not found: $NEW_CONFIG" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
export APPLY_TMP="$TMP"
|
|
20
|
+
export APPLY_FINAL="$OPENAGENT_CONFIG"
|
|
21
|
+
export APPLY_NEW="$NEW_CONFIG"
|
|
22
|
+
|
|
23
|
+
python3 <<'PYEOF'
|
|
24
|
+
import json, shutil, sys, os
|
|
25
|
+
|
|
26
|
+
tmp = os.path.expandvars(os.environ["APPLY_TMP"])
|
|
27
|
+
final = os.path.expandvars(os.environ["APPLY_FINAL"])
|
|
28
|
+
new_config = os.path.expandvars(os.environ["APPLY_NEW"])
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
with open(new_config) as f:
|
|
32
|
+
data = json.load(f)
|
|
33
|
+
|
|
34
|
+
missing = [k for k in ['agents', 'categories'] if k not in data]
|
|
35
|
+
if missing:
|
|
36
|
+
print('ERROR: Config missing required keys:', ', '.join(missing))
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
with open(tmp, 'w') as f:
|
|
40
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
41
|
+
f.write('\n')
|
|
42
|
+
|
|
43
|
+
shutil.move(tmp, final)
|
|
44
|
+
print('Config updated successfully')
|
|
45
|
+
|
|
46
|
+
except json.JSONDecodeError as e:
|
|
47
|
+
if os.path.exists(tmp):
|
|
48
|
+
os.remove(tmp)
|
|
49
|
+
print('ERROR: Invalid JSON — original config preserved:', e)
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
except SystemExit:
|
|
52
|
+
if os.path.exists(tmp):
|
|
53
|
+
os.remove(tmp)
|
|
54
|
+
raise
|
|
55
|
+
except Exception as e:
|
|
56
|
+
if os.path.exists(tmp):
|
|
57
|
+
os.remove(tmp)
|
|
58
|
+
print('ERROR:', e)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
PYEOF
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
4
|
+
source "$SCRIPT_DIR/../lib/config.sh"
|
|
5
|
+
|
|
6
|
+
if [ ! -f "$OPENAGENT_CONFIG" ]; then
|
|
7
|
+
echo "ERROR: Config not found at $OPENAGENT_CONFIG" >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
mkdir -p "$REBALANCER_ARCHIVE"
|
|
12
|
+
BACKUP="$REBALANCER_ARCHIVE/oh-my-openagent.$(date +%Y%m%d-%H%M%S).$$.json"
|
|
13
|
+
cp "$OPENAGENT_CONFIG" "$BACKUP"
|
|
14
|
+
echo "Archived: $BACKUP"
|
|
15
|
+
|
|
16
|
+
ARCHIVE_COUNT=$(find "$REBALANCER_ARCHIVE" -name 'oh-my-openagent.*.json' -type f | wc -l | tr -d ' ')
|
|
17
|
+
if [ "$ARCHIVE_COUNT" -gt 10 ]; then
|
|
18
|
+
echo "Cleaning up old archives (keeping 10 most recent)..."
|
|
19
|
+
cd "$REBALANCER_ARCHIVE"
|
|
20
|
+
DELETE_COUNT=$((ARCHIVE_COUNT - 10))
|
|
21
|
+
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
|
22
|
+
find . -name 'oh-my-openagent.*.json' -type f -exec stat -c '%Y %n' {} \; | sort -n | head -n "$DELETE_COUNT" | cut -d' ' -f2- | xargs rm -f
|
|
23
|
+
else
|
|
24
|
+
find . -name 'oh-my-openagent.*.json' -type f -exec stat -f '%m %N' {} \; | sort -n | head -n "$DELETE_COUNT" | cut -d' ' -f2- | xargs rm -f
|
|
25
|
+
fi
|
|
26
|
+
echo "Cleanup done."
|
|
27
|
+
fi
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
4
|
+
source "$SCRIPT_DIR/../lib/config.sh"
|
|
5
|
+
|
|
6
|
+
mkdir -p "$(dirname "$REBALANCER_CHANGELOG")"
|
|
7
|
+
if [ ! -f "$REBALANCER_CHANGELOG" ]; then
|
|
8
|
+
echo "# Model Rebalancer Changelog" > "$REBALANCER_CHANGELOG"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
printf '\n## %s\n\n' "$(date '+%Y-%m-%d %H:%M')"
|
|
13
|
+
cat
|
|
14
|
+
} >> "$REBALANCER_CHANGELOG"
|
|
15
|
+
|
|
16
|
+
echo "Changelog updated."
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
4
|
+
source "$SCRIPT_DIR/../lib/config.sh"
|
|
5
|
+
source "$SCRIPT_DIR/../lib/dates.sh"
|
|
6
|
+
|
|
7
|
+
if [ -z "${THRESHOLD_30D:-}" ] || [ -z "${THRESHOLD_7D:-}" ]; then
|
|
8
|
+
echo "ERROR: Failed to compute date thresholds (python3 required)" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
if [ ! -f "$OPENCODE_DB" ]; then
|
|
13
|
+
echo "ERROR: SQLite database not found at $OPENCODE_DB" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
DB="$OPENCODE_DB"
|
|
18
|
+
|
|
19
|
+
echo "=== Provider Distribution (30d) ==="
|
|
20
|
+
sqlite3 "$DB" <<SQL
|
|
21
|
+
SELECT
|
|
22
|
+
json_extract(data, '\$.providerID') as provider,
|
|
23
|
+
COUNT(*) as calls,
|
|
24
|
+
CASE WHEN (SELECT COUNT(*) FROM message
|
|
25
|
+
WHERE json_extract(data, '\$.role')='assistant'
|
|
26
|
+
AND json_extract(data, '\$.time.created') > $THRESHOLD_30D) = 0
|
|
27
|
+
THEN 0.0
|
|
28
|
+
ELSE ROUND(COUNT(*) * 100.0 / (
|
|
29
|
+
SELECT COUNT(*) FROM message
|
|
30
|
+
WHERE json_extract(data, '\$.role')='assistant'
|
|
31
|
+
AND json_extract(data, '\$.time.created') > $THRESHOLD_30D
|
|
32
|
+
), 1)
|
|
33
|
+
END as pct
|
|
34
|
+
FROM message
|
|
35
|
+
WHERE json_extract(data, '\$.role')='assistant'
|
|
36
|
+
AND json_extract(data, '\$.time.created') > $THRESHOLD_30D
|
|
37
|
+
GROUP BY provider ORDER BY calls DESC;
|
|
38
|
+
SQL
|
|
39
|
+
|
|
40
|
+
echo ""
|
|
41
|
+
echo "=== Provider Distribution (7d) ==="
|
|
42
|
+
sqlite3 "$DB" <<SQL
|
|
43
|
+
SELECT
|
|
44
|
+
json_extract(data, '\$.providerID') as provider,
|
|
45
|
+
COUNT(*) as calls,
|
|
46
|
+
CASE WHEN (SELECT COUNT(*) FROM message
|
|
47
|
+
WHERE json_extract(data, '\$.role')='assistant'
|
|
48
|
+
AND json_extract(data, '\$.time.created') > $THRESHOLD_7D) = 0
|
|
49
|
+
THEN 0.0
|
|
50
|
+
ELSE ROUND(COUNT(*) * 100.0 / (
|
|
51
|
+
SELECT COUNT(*) FROM message
|
|
52
|
+
WHERE json_extract(data, '\$.role')='assistant'
|
|
53
|
+
AND json_extract(data, '\$.time.created') > $THRESHOLD_7D
|
|
54
|
+
), 1)
|
|
55
|
+
END as pct
|
|
56
|
+
FROM message
|
|
57
|
+
WHERE json_extract(data, '\$.role')='assistant'
|
|
58
|
+
AND json_extract(data, '\$.time.created') > $THRESHOLD_7D
|
|
59
|
+
GROUP BY provider ORDER BY calls DESC;
|
|
60
|
+
SQL
|
|
61
|
+
|
|
62
|
+
echo ""
|
|
63
|
+
echo "=== Per-Agent Per-Provider Matrix (30d) ==="
|
|
64
|
+
sqlite3 "$DB" <<SQL
|
|
65
|
+
SELECT
|
|
66
|
+
json_extract(data, '\$.providerID') as provider,
|
|
67
|
+
json_extract(data, '\$.agent') as agent,
|
|
68
|
+
COUNT(*) as calls
|
|
69
|
+
FROM message
|
|
70
|
+
WHERE json_extract(data, '\$.role')='assistant'
|
|
71
|
+
AND json_extract(data, '\$.time.created') > $THRESHOLD_30D
|
|
72
|
+
GROUP BY provider, agent ORDER BY provider, calls DESC;
|
|
73
|
+
SQL
|
|
74
|
+
|
|
75
|
+
echo ""
|
|
76
|
+
echo "=== Agent Usage Ratio (30d) ==="
|
|
77
|
+
sqlite3 "$DB" <<SQL
|
|
78
|
+
SELECT
|
|
79
|
+
json_extract(data, '\$.agent') as agent,
|
|
80
|
+
COUNT(*) as total,
|
|
81
|
+
CASE WHEN (SELECT COUNT(*) FROM message
|
|
82
|
+
WHERE json_extract(data, '\$.role')='assistant'
|
|
83
|
+
AND json_extract(data, '\$.time.created') > $THRESHOLD_30D) = 0
|
|
84
|
+
THEN 0.0
|
|
85
|
+
ELSE ROUND(COUNT(*) * 100.0 / (
|
|
86
|
+
SELECT COUNT(*) FROM message
|
|
87
|
+
WHERE json_extract(data, '\$.role')='assistant'
|
|
88
|
+
AND json_extract(data, '\$.time.created') > $THRESHOLD_30D
|
|
89
|
+
), 1)
|
|
90
|
+
END as pct
|
|
91
|
+
FROM message
|
|
92
|
+
WHERE json_extract(data, '\$.role')='assistant'
|
|
93
|
+
AND json_extract(data, '\$.time.created') > $THRESHOLD_30D
|
|
94
|
+
GROUP BY agent ORDER BY total DESC;
|
|
95
|
+
SQL
|
|
96
|
+
|
|
97
|
+
echo ""
|
|
98
|
+
echo "=== Available Models ==="
|
|
99
|
+
opencode models 2>/dev/null || echo "(opencode models unavailable — skip model ID validation)"
|