Skip to content

Commit 7a10cbf

Browse files
authored
Merge pull request #10 from BaseInfinity/v0.10.4-small-model
v0.10.4: small_model top-level pin (--small-* flags)
2 parents 04d582d + a81a3cd commit 7a10cbf

6 files changed

Lines changed: 334 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,71 @@
22

33
All notable changes to opencode-sdlc-wizard.
44

5+
## [0.10.4] - 2026-05-18
6+
7+
### Added — `small_model` top-level pin (`--small-*` flags)
8+
9+
May-17 community-patterns research found 35–40% of surveyed
10+
`opencode.json` files set the top-level `small_model` field — OpenCode's
11+
cross-cutting hint for "use this when the call is cheap" (title
12+
generation, summary blurbs, anywhere a small/fast model suffices over
13+
the global `model`). Distinct from `agent.plan.model` (which only
14+
affects plan-mode tasks); `small_model` is consulted by any agent.
15+
16+
```bash
17+
# Set Opus as the build model, Haiku as the cheap fallback for
18+
# title/summary work
19+
npx opencode-sdlc-wizard pick \
20+
--tier proprietary --provider anthropic \
21+
--small-tier proprietary --small-provider anthropic --small-model claude-haiku-4-5
22+
```
23+
24+
Yields:
25+
26+
```json
27+
{
28+
"model": "anthropic/claude-opus-4-7",
29+
"small_model": "anthropic/claude-haiku-4-5",
30+
"provider": { "anthropic": { ... } }
31+
}
32+
```
33+
34+
`--small-*` composes cleanly with v0.10.0 reviewer, v0.10.1 sandboxes,
35+
v0.10.2 planner. Full v0.10.x stack in one call works.
36+
37+
### Changed — `scripts/configure-backend.sh`
38+
39+
- New `--small-tier T --small-provider P --small-model M` flags
40+
(all-or-nothing triplet; partial spec exits 2)
41+
- `small_model` value is `"<canonical_provider>/<model>"` using the same
42+
`PROVIDER_ALIASES` map
43+
- Small-side provider block deep-merges; same-provider as coder/reviewer/
44+
planner collapses to one block
45+
- Canonical key order updated: top-level is now `$schema`, `model`,
46+
`small_model`, `provider`, then rest alphabetical (matches the
47+
joelhooks + ppries community configs we surveyed — keeps the two
48+
top-level pins visually adjacent)
49+
50+
### Changed — `scripts/pick-backend.sh`
51+
52+
- New `--small-tier T --small-provider P [--small-model M]` flags
53+
- `--small-model` optional; filled from `default_model_for()` (same
54+
source of truth used for coder / reviewer / planner)
55+
- Partial-spec validation via the existing `validate_agent_triplet()`
56+
helper — no new error-handling code path
57+
58+
### Tests
59+
60+
- `tests/test-backend-picker.sh` adds T44–T48
61+
- `tests/test-pick.sh` adds T28–T32
62+
- **369 tests across 12 suites** (was 357 / 12 in v0.10.3)
63+
64+
### Compat
65+
66+
- 14 of 14 v0.10.3 default-model entries unchanged.
67+
- Opt-in only. Existing v0.9.x / v0.10.x configs unchanged unless
68+
`--small-*` flags are passed.
69+
570
## [0.10.3] - 2026-05-18
671

772
### Changed — two more research-verified default-model bumps

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-sdlc-wizard",
3-
"version": "0.10.3",
3+
"version": "0.10.4",
44
"description": "SDLC enforcement for OpenCode CLI — privacy-first, any-backend portability with a four-tier backend picker plus an OSS-tier cross-model-review skill so the full SDLC loop can run with zero Anthropic+OpenAI lock-in. Ships JSON Schemas for review artifacts so any consumer (cross-model-review, ditto, CI) can validate. Install with `npx opencode-sdlc-wizard init`. Sibling of agentic-sdlc-wizard and codex-sdlc-wizard.",
55
"bin": {
66
"opencode-sdlc-wizard": "cli/bin/opencode-sdlc-wizard.js"

scripts/configure-backend.sh

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ REVIEWER_MODEL=""
4040
PLANNER_TIER=""
4141
PLANNER_PROVIDER=""
4242
PLANNER_MODEL=""
43+
# v0.10.4 small_model: top-level fast/cheap fallback distinct from
44+
# agent.plan.model. Community signal: 35-40% of configs set this. Same
45+
# triplet shape, writes the top-level `small_model` field plus the
46+
# small-model provider block (deep-merged with coder/reviewer/planner).
47+
SMALL_TIER=""
48+
SMALL_PROVIDER=""
49+
SMALL_MODEL=""
4350
# v0.10.1 Per-agent permission sandboxing. Boolean flags inject canonical
4451
# permission.write blocks for the two highest-signal agents from May-2026
4552
# community-patterns research (9/15 configs use this): test-writer scoped
@@ -76,6 +83,12 @@ while [ $# -gt 0 ]; do
7683
--planner-provider=*) PLANNER_PROVIDER="${1#*=}" ;;
7784
--planner-model) shift; PLANNER_MODEL="${1:-}" ;;
7885
--planner-model=*) PLANNER_MODEL="${1#*=}" ;;
86+
--small-tier) shift; SMALL_TIER="${1:-}" ;;
87+
--small-tier=*) SMALL_TIER="${1#*=}" ;;
88+
--small-provider) shift; SMALL_PROVIDER="${1:-}" ;;
89+
--small-provider=*) SMALL_PROVIDER="${1#*=}" ;;
90+
--small-model) shift; SMALL_MODEL="${1:-}" ;;
91+
--small-model=*) SMALL_MODEL="${1#*=}" ;;
7992
--sandbox-test-writer) SANDBOX_TEST_WRITER=1 ;;
8093
--sandbox-docs) SANDBOX_DOCS=1 ;;
8194
-h|--help) usage; exit 0 ;;
@@ -110,6 +123,16 @@ if [ "$PL_SET" -ne 0 ] && [ "$PL_SET" -ne 3 ]; then
110123
exit 2
111124
fi
112125

126+
# v0.10.4 small-model validation.
127+
SM_SET=0
128+
[ -n "$SMALL_TIER" ] && SM_SET=$((SM_SET+1))
129+
[ -n "$SMALL_PROVIDER" ] && SM_SET=$((SM_SET+1))
130+
[ -n "$SMALL_MODEL" ] && SM_SET=$((SM_SET+1))
131+
if [ "$SM_SET" -ne 0 ] && [ "$SM_SET" -ne 3 ]; then
132+
echo "--small-tier / --small-provider / --small-model must all be set together (or none)" >&2
133+
exit 2
134+
fi
135+
113136
CONFIG_PATH="$TARGET_DIR/opencode.json"
114137

115138
# Build the provider-specific fragment as a JSON string. We keep this in a
@@ -118,20 +141,23 @@ CONFIG_PATH="$TARGET_DIR/opencode.json"
118141
node - "$TIER" "$PROVIDER" "$MODEL" "$CONFIG_PATH" "$FORCE" "$PRINT_ONLY" \
119142
"$REVIEWER_TIER" "$REVIEWER_PROVIDER" "$REVIEWER_MODEL" \
120143
"$SANDBOX_TEST_WRITER" "$SANDBOX_DOCS" \
121-
"$PLANNER_TIER" "$PLANNER_PROVIDER" "$PLANNER_MODEL" <<'NODE'
144+
"$PLANNER_TIER" "$PLANNER_PROVIDER" "$PLANNER_MODEL" \
145+
"$SMALL_TIER" "$SMALL_PROVIDER" "$SMALL_MODEL" <<'NODE'
122146
const fs = require("node:fs");
123147
const [
124148
tier, providerArg, model, configPath, forceStr, printOnlyStr,
125149
reviewerTier, reviewerProviderArg, reviewerModel,
126150
sandboxTestWriterStr, sandboxDocsStr,
127151
plannerTier, plannerProviderArg, plannerModel,
152+
smallTier, smallProviderArg, smallModel,
128153
] = process.argv.slice(2);
129154
const force = forceStr === "1";
130155
const printOnly = printOnlyStr === "1";
131156
const mixedMode = Boolean(reviewerTier && reviewerProviderArg && reviewerModel);
132157
const sandboxTestWriter = sandboxTestWriterStr === "1";
133158
const sandboxDocs = sandboxDocsStr === "1";
134159
const plannerMode = Boolean(plannerTier && plannerProviderArg && plannerModel);
160+
const smallMode = Boolean(smallTier && smallProviderArg && smallModel);
135161
136162
// Canonical provider IDs. Detector emits user-friendly aliases; we accept both
137163
// and emit the canonical OpenCode/models.dev ID in the written config so model
@@ -451,6 +477,17 @@ if (plannerMode) {
451477
});
452478
}
453479
480+
// v0.10.4 small_model: top-level field (not nested under agent). Distinct
481+
// from agent.plan.model — small_model is OpenCode's cross-cutting hint
482+
// for "use this when the call is cheap" (title generation, summary
483+
// blurbs, etc.). 35-40% of community configs set this.
484+
if (smallMode) {
485+
const smallProvider = PROVIDER_ALIASES[smallProviderArg] || smallProviderArg;
486+
const smallFragment = fragmentFor(smallTier, smallProvider, smallModel);
487+
merged.provider = deepMerge(merged.provider, smallFragment.provider || {});
488+
merged.small_model = smallFragment.model;
489+
}
490+
454491
// v0.10.1 Per-agent permission sandboxing. Each flag injects the canonical
455492
// permission.write pattern for that agent — deep-merged so it composes
456493
// with Mixed-Mode (--reviewer-*) and any user-set sibling fields. Patterns
@@ -489,7 +526,10 @@ function sortKeysCanonical(obj, topLevel = false) {
489526
const keys = Object.keys(obj);
490527
let ordered;
491528
if (topLevel) {
492-
const preferred = ["$schema", "model", "provider"];
529+
// v0.10.4: small_model lives right after `model` per the joelhooks
530+
// and ppries community configs we surveyed — keeps the two top-level
531+
// pins visually adjacent.
532+
const preferred = ["$schema", "model", "small_model", "provider"];
493533
const front = preferred.filter((k) => keys.includes(k));
494534
const rest = keys.filter((k) => !preferred.includes(k)).sort();
495535
ordered = [...front, ...rest];

scripts/pick-backend.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ REVIEWER_MODEL=""
5959
PLANNER_TIER=""
6060
PLANNER_PROVIDER=""
6161
PLANNER_MODEL=""
62+
# v0.10.4 small_model: global fast/cheap fallback. Same triplet shape;
63+
# writes top-level `small_model` (not under agent.). Per May-17 research,
64+
# 35-40% of community configs set this.
65+
SMALL_TIER=""
66+
SMALL_PROVIDER=""
67+
SMALL_MODEL=""
6268
# v0.10.1 Per-agent permission sandboxing. Passthrough to configure-backend's
6369
# matching flags — canonical permission.write block per agent (test/spec
6470
# files for test-writer, .md only for docs).
@@ -90,6 +96,12 @@ while [ $# -gt 0 ]; do
9096
--planner-provider=*) PLANNER_PROVIDER="${1#*=}" ;;
9197
--planner-model) shift; PLANNER_MODEL="${1:-}" ;;
9298
--planner-model=*) PLANNER_MODEL="${1#*=}" ;;
99+
--small-tier) shift; SMALL_TIER="${1:-}" ;;
100+
--small-tier=*) SMALL_TIER="${1#*=}" ;;
101+
--small-provider) shift; SMALL_PROVIDER="${1:-}" ;;
102+
--small-provider=*) SMALL_PROVIDER="${1#*=}" ;;
103+
--small-model) shift; SMALL_MODEL="${1:-}" ;;
104+
--small-model=*) SMALL_MODEL="${1#*=}" ;;
93105
--sandbox-test-writer) SANDBOX_TEST_WRITER=1 ;;
94106
--sandbox-docs) SANDBOX_DOCS=1 ;;
95107
-h|--help) usage; exit 0 ;;
@@ -208,6 +220,7 @@ validate_agent_triplet() {
208220
}
209221
validate_agent_triplet "reviewer" "$REVIEWER_TIER" "$REVIEWER_PROVIDER" "$REVIEWER_MODEL"
210222
validate_agent_triplet "planner" "$PLANNER_TIER" "$PLANNER_PROVIDER" "$PLANNER_MODEL"
223+
validate_agent_triplet "small" "$SMALL_TIER" "$SMALL_PROVIDER" "$SMALL_MODEL"
211224

212225
# Resolve reviewer-model default if --reviewer-tier + --reviewer-provider
213226
# set but --reviewer-model not. Same default-model map as the coder pin.
@@ -228,6 +241,15 @@ if [ -n "$PLANNER_TIER" ] && [ -n "$PLANNER_PROVIDER" ] && [ -z "$PLANNER_MODEL"
228241
}
229242
fi
230243

244+
# Same default-model fallback for the small (cheap/fast) side.
245+
if [ -n "$SMALL_TIER" ] && [ -n "$SMALL_PROVIDER" ] && [ -z "$SMALL_MODEL" ]; then
246+
SMALL_MODEL="$(default_model_for "$SMALL_TIER" "$SMALL_PROVIDER")" || {
247+
echo "pick: no default model known for small $SMALL_TIER/$SMALL_PROVIDER." >&2
248+
echo " Pass --small-model <name> explicitly." >&2
249+
exit 4
250+
}
251+
fi
252+
231253
# Forward to configure-backend.sh. --dry-run becomes --print-only on the
232254
# configurator (it prints the merged JSON but does not write the file).
233255
CONFIGURE_ARGS=(--tier "$TIER" --provider "$PROVIDER" --model "$MODEL")
@@ -248,6 +270,13 @@ if [ -n "$PLANNER_TIER" ]; then
248270
--planner-model "$PLANNER_MODEL"
249271
)
250272
fi
273+
if [ -n "$SMALL_TIER" ]; then
274+
CONFIGURE_ARGS+=(
275+
--small-tier "$SMALL_TIER"
276+
--small-provider "$SMALL_PROVIDER"
277+
--small-model "$SMALL_MODEL"
278+
)
279+
fi
251280
[ "$SANDBOX_TEST_WRITER" = "1" ] && CONFIGURE_ARGS+=(--sandbox-test-writer)
252281
[ "$SANDBOX_DOCS" = "1" ] && CONFIGURE_ARGS+=(--sandbox-docs)
253282

tests/test-backend-picker.sh

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,115 @@ console.log('ok');
928928
fi
929929
fi
930930

931+
# --- v0.10.4 small_model: global fast/cheap fallback. May-17 research found
932+
# 35-40% of community configs set this top-level field. Distinct from
933+
# agent.plan.model (which only affects plan-mode tasks) — small_model is
934+
# the cross-cutting "use this when the call is cheap" hint. Flags mirror
935+
# the reviewer/planner triplet shape: --small-tier T --small-provider P
936+
# [--small-model M, optional; filled from default-model map].
937+
938+
# --- T44: --small-* triplet writes top-level small_model + small provider block
939+
if [ -x "$CONFIG" ]; then
940+
T="$TMP_ROOT/t44"; mkdir -p "$T"
941+
(cd "$T" && "$CONFIG" \
942+
--tier proprietary --provider anthropic --model claude-opus-4-7 \
943+
--small-tier proprietary --small-provider anthropic --small-model claude-haiku-4-5 \
944+
>/dev/null 2>&1) || true
945+
if [ -f "$T/opencode.json" ]; then
946+
ok="$(node -e "
947+
const j=require('$T/opencode.json');
948+
if(j.model!=='anthropic/claude-opus-4-7'){console.log('coder-wrong:'+j.model);process.exit(1)}
949+
if(j.small_model!=='anthropic/claude-haiku-4-5'){console.log('small_model-wrong:'+j.small_model);process.exit(1)}
950+
console.log('ok');
951+
" 2>/dev/null || echo 'failed')"
952+
if [ "$ok" = "ok" ]; then
953+
pass "--small-* writes top-level small_model pin"
954+
else
955+
fail "T44 — $ok"
956+
fi
957+
fi
958+
fi
959+
960+
# --- T45: --small-* uses canonical alias (e.g., google_aistudio → google)
961+
if [ -x "$CONFIG" ]; then
962+
T="$TMP_ROOT/t45"; mkdir -p "$T"
963+
(cd "$T" && "$CONFIG" \
964+
--tier proprietary --provider anthropic --model claude-opus-4-7 \
965+
--small-tier proprietary --small-provider google_aistudio --small-model gemini-2.5-flash \
966+
>/dev/null 2>&1) || true
967+
if [ -f "$T/opencode.json" ]; then
968+
ok="$(node -e "
969+
const j=require('$T/opencode.json');
970+
if(j.small_model!=='google/gemini-2.5-flash'){console.log('alias-pin-wrong:'+j.small_model);process.exit(1)}
971+
if(!j.provider.google){console.log('alias-provider-missing');process.exit(1)}
972+
console.log('ok');
973+
" 2>/dev/null || echo 'failed')"
974+
if [ "$ok" = "ok" ]; then
975+
pass "--small-provider alias resolves to canonical (google_aistudio → google)"
976+
else
977+
fail "T45 — $ok"
978+
fi
979+
fi
980+
fi
981+
982+
# --- T46: --small-* composes with --reviewer-* + --planner-* + --sandbox-*
983+
if [ -x "$CONFIG" ]; then
984+
T="$TMP_ROOT/t46"; mkdir -p "$T"
985+
(cd "$T" && "$CONFIG" \
986+
--tier proprietary --provider anthropic --model claude-opus-4-7 \
987+
--small-tier proprietary --small-provider anthropic --small-model claude-haiku-4-5 \
988+
--reviewer-tier hosted_oss --reviewer-provider cerebras --reviewer-model gpt-oss-120b \
989+
--planner-tier hosted_oss --planner-provider groq --planner-model gpt-oss-120b \
990+
--sandbox-test-writer --sandbox-docs >/dev/null 2>&1) || true
991+
if [ -f "$T/opencode.json" ]; then
992+
ok="$(node -e "
993+
const j=require('$T/opencode.json');
994+
if(j.small_model!=='anthropic/claude-haiku-4-5'){console.log('small-wrong');process.exit(1)}
995+
if(j.agent.review.model!=='cerebras/gpt-oss-120b'){console.log('rev-wrong');process.exit(1)}
996+
if(j.agent.plan.model!=='groq/gpt-oss-120b'){console.log('plan-wrong');process.exit(1)}
997+
if(!j.agent['test-writer'].permission.write){console.log('tw-wrong');process.exit(1)}
998+
console.log('ok');
999+
" 2>/dev/null || echo 'failed')"
1000+
if [ "$ok" = "ok" ]; then
1001+
pass "Full v0.10.x: --small-* + --reviewer-* + --planner-* + --sandbox-* all compose"
1002+
else
1003+
fail "T46 — $ok"
1004+
fi
1005+
fi
1006+
fi
1007+
1008+
# --- T47: partial --small-* spec rejected (all-or-nothing triplet)
1009+
if [ -x "$CONFIG" ]; then
1010+
T="$TMP_ROOT/t47"; mkdir -p "$T"
1011+
rc=0
1012+
(cd "$T" && "$CONFIG" \
1013+
--tier private_local --provider ollama --model qwen3-coder:30b \
1014+
--small-tier proprietary >/dev/null 2>&1) || rc=$?
1015+
if [ "$rc" -ne 0 ] && [ ! -f "$T/opencode.json" ]; then
1016+
pass "partial --small-* spec rejected without writing opencode.json"
1017+
else
1018+
fail "T47 — partial small spec accepted (rc=$rc)"
1019+
fi
1020+
fi
1021+
1022+
# --- T48: no --small-* → no top-level small_model field (opt-in only)
1023+
if [ -x "$CONFIG" ]; then
1024+
T="$TMP_ROOT/t48"; mkdir -p "$T"
1025+
(cd "$T" && "$CONFIG" --tier private_local --provider ollama --model qwen3-coder:30b >/dev/null 2>&1) || true
1026+
if [ -f "$T/opencode.json" ]; then
1027+
ok="$(node -e "
1028+
const j=require('$T/opencode.json');
1029+
if('small_model' in j){console.log('unexpected-small_model');process.exit(1)}
1030+
console.log('ok');
1031+
" 2>/dev/null || echo 'failed')"
1032+
if [ "$ok" = "ok" ]; then
1033+
pass "no --small-* → no small_model field (opt-in regression guard)"
1034+
else
1035+
fail "T48 — $ok"
1036+
fi
1037+
fi
1038+
fi
1039+
9311040
echo ""
9321041
echo "=== Results: $PASS passed, $FAIL failed ==="
9331042
[ "$FAIL" -eq 0 ] || exit 1

0 commit comments

Comments
 (0)