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 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
+ }
@@ -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
+ }
@@ -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,6 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: source "$(dirname "$0")/lib/dates.sh"
3
+ set -euo pipefail
4
+
5
+ THRESHOLD_30D=$(python3 -c "import time; print(int((time.time() - 30*86400) * 1000))")
6
+ THRESHOLD_7D=$(python3 -c "import time; print(int((time.time() - 7*86400) * 1000))")
@@ -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)"
@@ -0,0 +1,9 @@
1
+ {
2
+ "last_updated": null,
3
+ "needs_update": true,
4
+ "providers": {},
5
+ "strategy": {
6
+ "summary": "",
7
+ "rules": []
8
+ }
9
+ }