Skip to content

Commit 984c7dc

Browse files
committed
feat: add assign-models utility and customization docs
1 parent 52ce503 commit 984c7dc

2 files changed

Lines changed: 211 additions & 0 deletions

File tree

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,52 @@ The default session model (set via `opencode -m`) should match the tier of work:
116116

117117
---
118118

119+
## 🔄 Customizing Models
120+
121+
OpenCode supports **any model provider** — switch the studio to your preferred
122+
models, including local ones, with a single command.
123+
124+
### Quick switch
125+
126+
```bash
127+
# Preview the change first
128+
node utils/assign-models.js --dry-run --map '{
129+
"opencode-go/kimi-k2.6": "anthropic/claude-opus-4",
130+
"opencode-go/qwen3.6-plus": "openai/gpt-4o",
131+
"opencode-go/deepseek-v4-flash": "ollama/llama3.2"
132+
}'
133+
134+
# Apply it
135+
node utils/assign-models.js --map '{
136+
"opencode-go/kimi-k2.6": "anthropic/claude-opus-4",
137+
"opencode-go/qwen3.6-plus": "openai/gpt-4o",
138+
"opencode-go/deepseek-v4-flash": "ollama/llama3.2"
139+
}'
140+
```
141+
142+
Or save your mapping to a JSON file and refer to it:
143+
144+
```bash
145+
node utils/assign-models.js --config my-models.json
146+
```
147+
148+
### Provider examples
149+
150+
| Provider | Model ID Format | Example |
151+
|----------|----------------|---------|
152+
| **OpenCode** (default) | `opencode-go/<model>` | `opencode-go/qwen3.6-plus` |
153+
| **Anthropic Claude** | `anthropic/<model>` | `anthropic/claude-opus-4`, `anthropic/claude-sonnet-4` |
154+
| **OpenAI** | `openai/<model>` | `openai/gpt-4o`, `openai/o3` |
155+
| **Google Gemini** | `google/<model>` | `google/gemini-2.5-pro` |
156+
| **Ollama** (local) | `ollama/<model>` | `ollama/llama3.2`, `ollama/mistral` |
157+
| **OpenAI-compatible** | `<endpoint>/<model>` | `http://localhost:11434/v1/llama3.2` |
158+
159+
> **Tip:** Run `opencode models` to list all models available through your
160+
> configured providers. See the [OpenCode provider docs](https://opencode.ai)
161+
> for setup instructions.
162+
163+
---
164+
119165
## 📁 Directory Tree
120166

121167
```
@@ -134,6 +180,8 @@ The default session model (set via `opencode -m`) should match the tier of work:
134180
├── design/ 🎨 Game design documents
135181
├── docs/ 📐 Technical documentation
136182
├── production/ 📊 Sprint plans, session logs
183+
├── utils/ 🔧 Developer utilities
184+
│ └── assign-models.js 🎯 Batch-model assignment tool
137185
└── ... 🎮 Game source & assets
138186
```
139187

utils/assign-models.js

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/env node
2+
const fs = require("fs");
3+
const path = require("path");
4+
5+
const AGENTS_DIR = path.resolve(__dirname, "..", ".opencode", "agents");
6+
const MODEL_RE = /^(model:\s*).+/m;
7+
8+
function usage() {
9+
console.log(`
10+
Usage: node utils/assign-models.js --map <json> [--dry-run]
11+
node utils/assign-models.js --config <file> [--dry-run]
12+
13+
Map 49 agent models to your preferred providers in one shot.
14+
15+
Options:
16+
--map <json> Inline mapping: {"old-model":"new-model",...}
17+
--config <file> JSON config file with same structure
18+
--dry-run Preview changes without writing files
19+
20+
Examples:
21+
node utils/assign-models.js --dry-run --map '{
22+
"opencode-go/kimi-k2.6": "anthropic/claude-opus-4",
23+
"opencode-go/qwen3.6-plus": "openai/gpt-4o",
24+
"opencode-go/deepseek-v4-flash": "ollama/llama3.2"
25+
}'
26+
27+
node utils/assign-models.js --config my-models.json
28+
`);
29+
process.exit(0);
30+
}
31+
32+
function parseArgs() {
33+
const args = process.argv.slice(2);
34+
if (args.includes("--help") || args.includes("-h")) usage();
35+
const dryRun = args.includes("--dry-run");
36+
37+
let raw;
38+
const mapIdx = args.indexOf("--map");
39+
const cfgIdx = args.indexOf("--config");
40+
41+
if (mapIdx !== -1 && cfgIdx !== -1) {
42+
console.error("error: use --map OR --config, not both");
43+
process.exit(1);
44+
}
45+
46+
if (mapIdx !== -1) {
47+
raw = args[mapIdx + 1];
48+
if (!raw) { console.error("error: --map requires a JSON argument"); process.exit(1); }
49+
} else if (cfgIdx !== -1) {
50+
const cfgPath = args[cfgIdx + 1];
51+
if (!cfgPath) { console.error("error: --config requires a file path"); process.exit(1); }
52+
raw = fs.readFileSync(path.resolve(cfgPath), "utf8");
53+
} else {
54+
console.error("error: provide --map or --config");
55+
process.exit(1);
56+
}
57+
58+
let mapping;
59+
try { mapping = JSON.parse(raw); } catch {
60+
console.error("error: invalid JSON");
61+
process.exit(1);
62+
}
63+
64+
return { mapping, dryRun };
65+
}
66+
67+
function readAgentFiles() {
68+
const files = fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith(".md"));
69+
const agents = [];
70+
71+
for (const file of files) {
72+
const fullPath = path.join(AGENTS_DIR, file);
73+
const content = fs.readFileSync(fullPath, "utf8");
74+
const match = content.match(MODEL_RE);
75+
if (match) {
76+
const model = match[0].replace(/^model:\s*/, "").trim();
77+
agents.push({ file, fullPath, content, model, line: match[0] });
78+
}
79+
}
80+
81+
return agents;
82+
}
83+
84+
function groupByModel(agents) {
85+
const groups = {};
86+
for (const a of agents) {
87+
if (!groups[a.model]) groups[a.model] = [];
88+
groups[a.model].push(a.file);
89+
}
90+
return groups;
91+
}
92+
93+
function applyMapping(agents, mapping, dryRun) {
94+
const changes = [];
95+
96+
for (const agent of agents) {
97+
const newModel = mapping[agent.model];
98+
if (!newModel) continue;
99+
const newLine = agent.line.replace(MODEL_RE, `model: ${newModel}`);
100+
if (newLine === agent.line) continue;
101+
102+
changes.push({
103+
file: agent.file,
104+
old: agent.model,
105+
new: newModel
106+
});
107+
108+
if (!dryRun) {
109+
const updated = agent.content.replace(MODEL_RE, `model: ${newModel}`);
110+
fs.writeFileSync(agent.fullPath, updated, "utf8");
111+
}
112+
}
113+
114+
return changes;
115+
}
116+
117+
function main() {
118+
const { mapping, dryRun } = parseArgs();
119+
120+
if (!fs.existsSync(AGENTS_DIR)) {
121+
console.error(`error: agents directory not found at ${AGENTS_DIR}`);
122+
process.exit(1);
123+
}
124+
125+
const agents = readAgentFiles();
126+
const before = groupByModel(agents);
127+
const changes = applyMapping(agents, mapping, dryRun);
128+
129+
console.log(`\nAgent files scanned: ${agents.length}`);
130+
console.log("\nBefore:");
131+
for (const [model, names] of Object.entries(before)) {
132+
console.log(` ${model} (${names.length})`);
133+
}
134+
135+
if (changes.length === 0) {
136+
console.log("\nNo changes — mapping keys don't match any current models.");
137+
console.log("Current models:", [...new Set(agents.map(a => a.model))].join(", "));
138+
return;
139+
}
140+
141+
console.log(`\n${dryRun ? "[DRY RUN] " : ""}Changes (${changes.length} files):`);
142+
for (const c of changes) {
143+
console.log(` ${c.file.padEnd(32)} ${c.old.padEnd(35)}${c.new}`);
144+
}
145+
146+
const after = groupByModel(
147+
agents.map(a => ({
148+
...a,
149+
model: mapping[a.model] || a.model
150+
}))
151+
);
152+
console.log("\nAfter:");
153+
for (const [model, names] of Object.entries(after)) {
154+
console.log(` ${model} (${names.length})`);
155+
}
156+
157+
if (!dryRun) {
158+
console.log(`\nDone. Wrote ${changes.length} files.`);
159+
console.log("Run with --dry-run to preview before committing.");
160+
}
161+
}
162+
163+
main();

0 commit comments

Comments
 (0)