opencode-harness-kit 0.2.1 → 0.3.3
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/CHANGELOG.md +301 -0
- package/GLOBAL.md +50 -13
- package/MIGRATION.md +140 -0
- package/README.md +233 -116
- package/bin/oc-harness.mjs +474 -96
- package/dist/index.js +378 -155
- package/docs/KNOWN-LIMITS.md +189 -4
- package/docs/RESEARCH.md +80 -5
- package/docs/ROUTING.md +254 -0
- package/lanes.json +136 -0
- package/lanes.schema.json +55 -0
- package/package.json +12 -8
- package/profiles/beam-free.json +51 -0
- package/profiles/big-pickle.json +45 -0
- package/profiles/deepseek-v4.1-flash-free.json +49 -0
- package/profiles/fleet-all.json +139 -0
- package/profiles/fleet-safe.json +139 -0
- package/profiles/glm-5.3-flash-free.json +49 -0
- package/profiles/index.json +120 -37
- package/profiles/longcat-2.5-preview-free.json +14 -10
- package/profiles/mimo-v2.6-flash-free.json +44 -0
- package/profiles/minimax-m3.1-flash-free.json +51 -0
- package/profiles/nemotron-3-ultra-free.json +44 -0
- package/profiles/nemotron-3.5-lightning-free.json +44 -0
- package/profiles/nemotron-3.5-super-vl-free.json +52 -0
- package/profiles/schema.json +30 -11
- package/{agents → prompts}/build.md +2 -35
- package/prompts/doc.md +42 -0
- package/{agents → prompts}/locate.md +1 -16
- package/prompts/longform.md +34 -0
- package/{agents → prompts}/plan.md +2 -25
- package/{agents → prompts}/reviewer.md +2 -22
- package/prompts/schema.md +30 -0
- package/prompts/sweep.md +29 -0
- package/{agents → prompts}/verify.md +6 -29
- package/prompts/vision.md +33 -0
- package/retention.yml +75 -57
- package/skills/space-build/SKILL.md +16 -0
- package/skills/space-review/SKILL.md +8 -0
- package/src/config.js +94 -69
- package/src/effort.js +59 -22
- package/src/index.ts +15 -2
- package/src/lane-usage.js +142 -0
- package/src/profile.js +502 -212
- package/src/router.ts +148 -39
- package/src/telemetry.ts +98 -0
- package/examples/muse-spark-1.3-contributor-free.json +0 -41
- package/profiles/deepseek-v4-flash-free.json +0 -41
- package/profiles/muse-split-free.json +0 -64
- package/profiles/space-bunny-free.json +0 -42
- package/profiles/x-preview-f-free.json +0 -41
package/src/router.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
// Model fallback across a profile's
|
|
1
|
+
// Model fallback across a profile's lanes.
|
|
2
2
|
//
|
|
3
3
|
// The honest version of this feature: opencode has no hook that can change a
|
|
4
4
|
// live session's model. `chat.params` only sees temperature and sampling, the
|
|
5
5
|
// model on `chat.message` is read-only, and `config` runs once at startup. So
|
|
6
6
|
// failover here means two things, and only two:
|
|
7
7
|
//
|
|
8
|
-
// 1. At startup, if a health file marks
|
|
9
|
-
// the next
|
|
8
|
+
// 1. At startup, if a health file marks a route unhealthy, the lane that
|
|
9
|
+
// used it moves to the next rung of its own chain. A fleet has ten lanes
|
|
10
|
+
// and ten different models, so one spent route must not take the other
|
|
11
|
+
// nine down with it.
|
|
10
12
|
// 2. When a request fails, tell the agent plainly that the route is spent and
|
|
11
13
|
// a new session is needed, instead of letting it retry into a wall.
|
|
12
14
|
//
|
|
@@ -25,6 +27,9 @@ import {
|
|
|
25
27
|
profileIdFromConfig,
|
|
26
28
|
reservedTokens,
|
|
27
29
|
resolveEffort,
|
|
30
|
+
resolveLaneChain,
|
|
31
|
+
resolveLaneModel,
|
|
32
|
+
resolveRoles,
|
|
28
33
|
} from "./profile.js"
|
|
29
34
|
|
|
30
35
|
interface Health {
|
|
@@ -53,6 +58,29 @@ function writeHealth(worktree: string, h: Health) {
|
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The models a previous session found spent.
|
|
63
|
+
*
|
|
64
|
+
* Exported so the effort hook resolves a lane's variant against the model the
|
|
65
|
+
* lane actually ended up on. A lane that failed over from a model with a
|
|
66
|
+
* `none` effort level to one without levels has to lose its variant, or the
|
|
67
|
+
* config asks for an effort the new model does not publish.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} worktree
|
|
70
|
+
* @returns {Set<string>}
|
|
71
|
+
*/
|
|
72
|
+
export function unhealthyModels(worktree: string): Set<string> {
|
|
73
|
+
const health = readHealth(worktree)
|
|
74
|
+
// The health file is keyed by the qualified route, `opencode/<model>`. The
|
|
75
|
+
// lane chains list bare model ids, so the prefix is dropped here. Comparing
|
|
76
|
+
// the two directly would never match, and a spent route would look healthy.
|
|
77
|
+
const out = new Set<string>()
|
|
78
|
+
for (const key of Object.keys(health.unhealthy ?? {})) {
|
|
79
|
+
out.add(key.includes("/") ? key.slice(key.indexOf("/") + 1) : key)
|
|
80
|
+
}
|
|
81
|
+
return out
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
/** Patterns that mean "this route is spent", not "this request was bad". */
|
|
57
85
|
const SPENT = [
|
|
58
86
|
/FreeUsageLimitError/i,
|
|
@@ -72,8 +100,8 @@ function classify(text: string): string | null {
|
|
|
72
100
|
const WARN_TEXT =
|
|
73
101
|
"[opencode-harness-kit] This session's model route is rate limited or withdrawn. " +
|
|
74
102
|
"No plugin can swap a live session's model, so stop retrying and ask the human to start a " +
|
|
75
|
-
"new session. Run `oc-harness list` to see the
|
|
76
|
-
"check whether the profile still matches live metadata."
|
|
103
|
+
"new session. Run `oc-harness list` to see the lanes and their chains, or `oc-harness doctor` " +
|
|
104
|
+
"to check whether the profile still matches live metadata."
|
|
77
105
|
|
|
78
106
|
export const RouterPlugin: Plugin = async ({ worktree }) => {
|
|
79
107
|
let failedSessions = 0
|
|
@@ -85,32 +113,75 @@ export const RouterPlugin: Plugin = async ({ worktree }) => {
|
|
|
85
113
|
try {
|
|
86
114
|
const cfg = config as Record<string, any>
|
|
87
115
|
const health = readHealth(worktree)
|
|
116
|
+
const unhealthy = unhealthyModels(worktree)
|
|
88
117
|
|
|
89
118
|
// config.harness is stripped by schema validation, so the active
|
|
90
119
|
// profile comes from the config file on disk.
|
|
91
120
|
const activeId = cfg.harness?.profile ?? profileIdFromConfig()
|
|
92
|
-
if (activeId)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
121
|
+
if (!activeId) return
|
|
122
|
+
const id: string = activeId
|
|
123
|
+
let profile
|
|
124
|
+
try {
|
|
125
|
+
profile = loadProfile(id)
|
|
126
|
+
} catch {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 1. The session's own model. When it is spent, the profile's
|
|
131
|
+
// fallback queue names the profiles that can take over.
|
|
132
|
+
//
|
|
133
|
+
// A fleet keeps its lanes. Every lane has its own chain, and step 2
|
|
134
|
+
// moves a lane down that chain when its route is spent, so
|
|
135
|
+
// overwriting the lanes here would collapse ten jobs onto one
|
|
136
|
+
// model and throw away the chain that was supposed to survive the
|
|
137
|
+
// outage. Only the session model moves.
|
|
138
|
+
const baseKey = `opencode/${profile.model}`
|
|
139
|
+
if (health.unhealthy?.[baseKey]) {
|
|
140
|
+
const queue = [id, ...(profile.fallback ?? [])]
|
|
141
|
+
for (const candidate of queue) {
|
|
142
|
+
if (health.unhealthy?.[`opencode/${candidate}`]) continue
|
|
143
|
+
let fallback
|
|
98
144
|
try {
|
|
99
|
-
|
|
100
|
-
for (const candidate of queue) {
|
|
101
|
-
if (!health.unhealthy[`opencode/${candidate}`]) {
|
|
102
|
-
chosen = candidate
|
|
103
|
-
break
|
|
104
|
-
}
|
|
105
|
-
}
|
|
145
|
+
fallback = loadProfile(candidate)
|
|
106
146
|
} catch {
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
if (chosen !== id) {
|
|
110
|
-
applyProfileToConfig(cfg, chosen)
|
|
111
|
-
health.active = chosen
|
|
112
|
-
writeHealth(worktree, health)
|
|
147
|
+
continue
|
|
113
148
|
}
|
|
149
|
+
const key = `opencode/${fallback.model}`
|
|
150
|
+
if (health.unhealthy?.[key]) continue
|
|
151
|
+
const fleet = isFleet(profile)
|
|
152
|
+
applySessionModel(cfg, fallback, {
|
|
153
|
+
keepLanes: fleet,
|
|
154
|
+
// The lanes did not change, so the numbers derived from them did
|
|
155
|
+
// not change either.
|
|
156
|
+
profileId: fleet ? profile.id : fallback.id,
|
|
157
|
+
reserved: fleet ? reservedTokens(profile) : undefined,
|
|
158
|
+
threshold: fleet ? pruneThreshold(profile) : undefined,
|
|
159
|
+
})
|
|
160
|
+
health.active = candidate
|
|
161
|
+
writeHealth(worktree, health)
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 2. Every lane. A lane whose model is spent moves to the next rung of
|
|
167
|
+
// its own chain, so one dead route costs one lane, not the fleet.
|
|
168
|
+
const roles = resolveRoles(profile, { unhealthy })
|
|
169
|
+
for (const [lane, level] of Object.entries(roles)) {
|
|
170
|
+
if (level === undefined) continue
|
|
171
|
+
const agent = cfg.agent?.[lane]
|
|
172
|
+
if (!agent) continue
|
|
173
|
+
const resolved = resolveLaneModel(profile, lane, { unhealthy })
|
|
174
|
+
if (resolved.model === profile.model && !profile.roleModels?.[lane]) continue
|
|
175
|
+
const wantModel = `opencode/${resolved.model}`
|
|
176
|
+
if (agent.model !== wantModel) {
|
|
177
|
+
agent.model = wantModel
|
|
178
|
+
// The variant has to be resolved against the model the lane ended
|
|
179
|
+
// up on. A lane that failed over to a model with fewer effort
|
|
180
|
+
// levels has to lose the variant the old model had.
|
|
181
|
+
const requested = (profile.roles?.[lane] ?? null) as any
|
|
182
|
+
const next = resolveEffort(resolved.levels, requested)
|
|
183
|
+
if (next === null) delete agent.variant
|
|
184
|
+
else agent.variant = next
|
|
114
185
|
}
|
|
115
186
|
}
|
|
116
187
|
} catch {
|
|
@@ -160,30 +231,68 @@ export const RouterPlugin: Plugin = async ({ worktree }) => {
|
|
|
160
231
|
}
|
|
161
232
|
}
|
|
162
233
|
|
|
163
|
-
/**
|
|
164
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Whether a profile runs more than one model behind its lanes.
|
|
236
|
+
* @param {any} profile
|
|
237
|
+
* @returns {boolean}
|
|
238
|
+
*/
|
|
239
|
+
function isFleet(profile: any): boolean {
|
|
240
|
+
return Object.keys(profile?.roleModels ?? {}).length > 0
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Move the SESSION onto another profile's model.
|
|
245
|
+
*
|
|
246
|
+
* A fleet keeps its lanes. The lanes have their own chains, and the config
|
|
247
|
+
* hook has already moved each one down its chain past a spent route, so
|
|
248
|
+
* overwriting them here would collapse ten jobs onto one model and discard
|
|
249
|
+
* the chain that was supposed to survive the outage.
|
|
250
|
+
*
|
|
251
|
+
* A profile with no lanes of its own is a single-model profile: there is
|
|
252
|
+
* nothing to keep, so the fallback takes the lanes too.
|
|
253
|
+
*
|
|
254
|
+
* The session model is published on `harness.sessionModel` so the effort hook
|
|
255
|
+
* - which runs after this one - pins the session to the model that was chosen
|
|
256
|
+
* here rather than back to the base model that is spent.
|
|
257
|
+
*/
|
|
258
|
+
function applySessionModel(
|
|
259
|
+
cfg: Record<string, any>,
|
|
260
|
+
profile: any,
|
|
261
|
+
opts: {
|
|
262
|
+
keepLanes?: boolean
|
|
263
|
+
profileId?: string
|
|
264
|
+
reserved?: number
|
|
265
|
+
threshold?: number
|
|
266
|
+
} = {},
|
|
267
|
+
) {
|
|
165
268
|
try {
|
|
166
|
-
const
|
|
167
|
-
const model = `${p.provider}/${p.model}`
|
|
269
|
+
const model = `${profile.provider}/${profile.model}`
|
|
168
270
|
cfg.model = model
|
|
169
271
|
cfg.small_model = model
|
|
170
|
-
const variant =
|
|
171
|
-
if (
|
|
272
|
+
const variant = profile.effort?.default
|
|
273
|
+
if (profile.effort?.levels?.length && variant) cfg.variant = variant
|
|
172
274
|
else delete cfg.variant
|
|
173
275
|
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
276
|
+
if (!opts.keepLanes) {
|
|
277
|
+
for (const [lane, want] of Object.entries(profile.roles ?? {})) {
|
|
278
|
+
const agent = cfg.agent ?? (cfg.agent = {})
|
|
279
|
+
const existing = agent[lane] ?? (agent[lane] = {})
|
|
280
|
+
existing.model = model
|
|
281
|
+
const level = resolveEffort(profile.effort?.levels ?? [], want as any)
|
|
282
|
+
if (level) existing.variant = level
|
|
283
|
+
else delete existing.variant
|
|
284
|
+
}
|
|
181
285
|
}
|
|
182
286
|
if (cfg.compaction) {
|
|
183
287
|
cfg.compaction.prune = true
|
|
184
|
-
cfg.compaction.reserved = reservedTokens(
|
|
288
|
+
cfg.compaction.reserved = opts.reserved ?? reservedTokens(profile)
|
|
289
|
+
}
|
|
290
|
+
cfg.harness = {
|
|
291
|
+
...(cfg.harness ?? {}),
|
|
292
|
+
profile: opts.profileId ?? profile.id,
|
|
293
|
+
threshold: opts.threshold ?? pruneThreshold(profile),
|
|
294
|
+
sessionModel: model,
|
|
185
295
|
}
|
|
186
|
-
cfg.harness = { ...(cfg.harness ?? {}), profile: profileId, threshold: pruneThreshold(p) }
|
|
187
296
|
} catch {
|
|
188
297
|
// Leave the config alone if the fallback profile cannot be read.
|
|
189
298
|
}
|
package/src/telemetry.ts
CHANGED
|
@@ -8,6 +8,29 @@
|
|
|
8
8
|
// What is worth recording: context size per request (to see pruning working),
|
|
9
9
|
// latency by role and effort (to see tiering paying off), tool denials (to see
|
|
10
10
|
// the guard being useful rather than annoying), and provider rate limits.
|
|
11
|
+
//
|
|
12
|
+
// A fleet adds one more question, and it is the one that decides whether the
|
|
13
|
+
// fleet stays at ten lanes or is cut down: which lane is actually used. Ten
|
|
14
|
+
// lanes that are never called are ten descriptions in the model\'s context and
|
|
15
|
+
// ten models on the free tier doing nothing. `oc-harness lanes` reads these
|
|
16
|
+
// records and reports the ones that were never called.
|
|
17
|
+
//
|
|
18
|
+
// WHAT A CALL IS
|
|
19
|
+
//
|
|
20
|
+
// One assistant message is one call. opencode emits `message.updated` several
|
|
21
|
+
// times for a single message - created, streamed, completed - so writing a
|
|
22
|
+
// record per event counted every call three times. A lane record therefore
|
|
23
|
+
// carries the message id, and a lane is counted once per id.
|
|
24
|
+
//
|
|
25
|
+
// Two records are written per message, at most: an open one when the message
|
|
26
|
+
// appears, and a closed one when it completes. The closed record is the only
|
|
27
|
+
// one with a duration, so "how long does this lane take" and "was this lane
|
|
28
|
+
// called at all" are answered from the same file without guessing.
|
|
29
|
+
//
|
|
30
|
+
// The `chat.message` hook is deliberately not used for this. Measured on
|
|
31
|
+
// OpenCode 1.19, its `agent` field is undefined, so it cannot attribute a
|
|
32
|
+
// call to a lane; and a second counter that cannot be correlated with the
|
|
33
|
+
// first would double-count every call.
|
|
11
34
|
|
|
12
35
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
13
36
|
import { appendFileSync, mkdirSync, existsSync, statSync, renameSync } from "node:fs"
|
|
@@ -45,6 +68,17 @@ export const TelemetryPlugin: Plugin = async ({ worktree }) => {
|
|
|
45
68
|
const dir = join(worktree, ".space")
|
|
46
69
|
const started = Date.now()
|
|
47
70
|
|
|
71
|
+
// The message id of every lane record already written, and of every one
|
|
72
|
+
// already written with a duration.
|
|
73
|
+
//
|
|
74
|
+
// opencode emits `message.updated` several times for a single assistant
|
|
75
|
+
// message: once when the message is created, once per streaming update, and
|
|
76
|
+
// once when it completes. Writing a record per event counted one call three
|
|
77
|
+
// times, which made the "cut the lanes nobody calls" counter wrong by a
|
|
78
|
+
// factor of three. A lane is counted once per message id instead.
|
|
79
|
+
const opened = new Set<string>()
|
|
80
|
+
const closed = new Set<string>()
|
|
81
|
+
|
|
48
82
|
return {
|
|
49
83
|
event: async ({ event }) => {
|
|
50
84
|
// Events are provider-specific and opencode may add shapes we have never
|
|
@@ -56,6 +90,30 @@ export const TelemetryPlugin: Plugin = async ({ worktree }) => {
|
|
|
56
90
|
// they get logged even though the event shape is provider-specific.
|
|
57
91
|
if (/error|rate|limit|quota/i.test(type)) {
|
|
58
92
|
write(dir, { kind: "provider-event", type, detail: truncate(safeStringify(event)) })
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// An assistant message is the only place the kit can see which lane ran,
|
|
97
|
+
// on which model, and how long it took. That record is what makes "cut
|
|
98
|
+
// the lanes nobody uses" a measurement instead of a guess.
|
|
99
|
+
if (type === "message.updated") {
|
|
100
|
+
const lane = laneOf(event)
|
|
101
|
+
if (!lane || !lane.id) return
|
|
102
|
+
if (lane.ms !== undefined) {
|
|
103
|
+
// The completed event. Written once, and only if the open record
|
|
104
|
+
// did not already carry a duration.
|
|
105
|
+
if (closed.has(lane.id)) return
|
|
106
|
+
closed.add(lane.id)
|
|
107
|
+
write(dir, { kind: "lane", ...lane })
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
// The open event: the lane was called. Written even if the call never
|
|
111
|
+
// finishes, because "this lane is useless" and "this lane is slow"
|
|
112
|
+
// are different questions and only the first one is settled by a
|
|
113
|
+
// record with no duration.
|
|
114
|
+
if (opened.has(lane.id)) return
|
|
115
|
+
opened.add(lane.id)
|
|
116
|
+
write(dir, { kind: "lane", ...lane })
|
|
59
117
|
}
|
|
60
118
|
},
|
|
61
119
|
|
|
@@ -77,6 +135,46 @@ export const TelemetryPlugin: Plugin = async ({ worktree }) => {
|
|
|
77
135
|
}
|
|
78
136
|
}
|
|
79
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The lane, model and duration of one assistant message, read defensively.
|
|
140
|
+
*
|
|
141
|
+
* The event shape is opencode's, not ours, and it has changed between
|
|
142
|
+
* releases. Every field is optional and a shape we do not recognise yields
|
|
143
|
+
* null rather than a wrong number.
|
|
144
|
+
*
|
|
145
|
+
* The message id is what makes the record countable: one message is one call.
|
|
146
|
+
*/
|
|
147
|
+
function laneOf(event: unknown): {
|
|
148
|
+
id?: string
|
|
149
|
+
agent?: string
|
|
150
|
+
model?: string
|
|
151
|
+
provider?: string
|
|
152
|
+
ms?: number
|
|
153
|
+
} | null {
|
|
154
|
+
const e = event as { properties?: any } | undefined
|
|
155
|
+
const info = e?.properties?.info ?? e?.properties
|
|
156
|
+
if (!info || typeof info !== "object") return null
|
|
157
|
+
if (info.role && info.role !== "assistant") return null
|
|
158
|
+
|
|
159
|
+
const id = typeof info.id === "string" ? info.id : undefined
|
|
160
|
+
const agent = typeof info.agent === "string" ? info.agent : undefined
|
|
161
|
+
const modelId = info.model?.modelID ?? info.modelID
|
|
162
|
+
const providerId = info.model?.providerID ?? info.providerID
|
|
163
|
+
const time = info.time ?? {}
|
|
164
|
+
const created = typeof time.created === "number" ? time.created : undefined
|
|
165
|
+
const completed = typeof time.completed === "number" ? time.completed : undefined
|
|
166
|
+
const ms = created !== undefined && completed !== undefined ? completed - created : undefined
|
|
167
|
+
|
|
168
|
+
if (!id || !agent) return null
|
|
169
|
+
return {
|
|
170
|
+
id,
|
|
171
|
+
agent,
|
|
172
|
+
model: typeof modelId === "string" ? modelId : undefined,
|
|
173
|
+
provider: typeof providerId === "string" ? providerId : undefined,
|
|
174
|
+
ms: typeof ms === "number" && ms >= 0 ? ms : undefined,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
80
178
|
function extractRule(output: string): string {
|
|
81
179
|
const m = output.match(/blocked:\s*([^\n]+)/)
|
|
82
180
|
return m ? m[1].trim() : "unknown"
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$comment": "Contrarian example. Kept out of profiles/index.json on purpose: free, 1M context, and widely used, but its provider is permitted to train on your prompts and it has no `max` effort. `oc-harness list` shows it struck through with the reason. Install it with `oc-harness use muse-spark-1.3-contributor-free` and the warning is repeated on install.",
|
|
3
|
-
"id": "muse-spark-1.3-contributor-free",
|
|
4
|
-
"title": "Muse Spark 1.3 Contributor Free (OpenCode Zen)",
|
|
5
|
-
"provider": "opencode",
|
|
6
|
-
"model": "muse-spark-1.3-contributor-free",
|
|
7
|
-
"family": "muse-free",
|
|
8
|
-
"verified": "2026-10-07",
|
|
9
|
-
"retention": "may-train",
|
|
10
|
-
"notes": "Capability/privacy tradeoff, not a recommendation. Served over /v1/responses, a better cache dialect than chat/completions, and it supports temperature. But: 'Heavily discounted token pricing in exchange for permission to use your prompts and completions to train future Meta models', and its top effort is xhigh rather than max. Do not make this the default for private code.",
|
|
11
|
-
"limits": {
|
|
12
|
-
"context": 1048576,
|
|
13
|
-
"output": 131072
|
|
14
|
-
},
|
|
15
|
-
"supports": {
|
|
16
|
-
"reasoning": true,
|
|
17
|
-
"temperature": true,
|
|
18
|
-
"toolCall": true,
|
|
19
|
-
"structuredOutput": true,
|
|
20
|
-
"attachment": true
|
|
21
|
-
},
|
|
22
|
-
"effort": {
|
|
23
|
-
"levels": ["minimal", "low", "medium", "high", "xhigh"],
|
|
24
|
-
"default": "xhigh",
|
|
25
|
-
"reserveShare": 0.12
|
|
26
|
-
},
|
|
27
|
-
"roles": {
|
|
28
|
-
"plan": "max",
|
|
29
|
-
"build": "high",
|
|
30
|
-
"reviewer": "xhigh",
|
|
31
|
-
"locate": "low",
|
|
32
|
-
"verify": "low"
|
|
33
|
-
},
|
|
34
|
-
"prune": {
|
|
35
|
-
"enabled": true,
|
|
36
|
-
"keepRecentToolCalls": 6,
|
|
37
|
-
"keepRecentTurns": 3,
|
|
38
|
-
"keepUserMessages": true
|
|
39
|
-
},
|
|
40
|
-
"fallback": ["x-preview-f-free", "longcat-2.5-preview-free"]
|
|
41
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$comment": "Fallback tier 3. The smallest context in the queue at 200k, which is why prune.keepRecentToolCalls drops: the same settings tuned for a 1M model overflow this one.",
|
|
3
|
-
"id": "deepseek-v4-flash-free",
|
|
4
|
-
"title": "DeepSeek V4 Flash Free (OpenCode Zen)",
|
|
5
|
-
"provider": "opencode",
|
|
6
|
-
"model": "deepseek-v4-flash-free",
|
|
7
|
-
"family": "deepseek-flash",
|
|
8
|
-
"verified": "2026-10-07",
|
|
9
|
-
"retention": "zero-retention",
|
|
10
|
-
"notes": "Text only, no attachment, 200k context. Included to show the compiler scaling pruning to the model: reserveShare and retention fall together, so a 1M-tuned profile cannot silently overflow a 200k model.",
|
|
11
|
-
"limits": {
|
|
12
|
-
"context": 200000,
|
|
13
|
-
"output": 128000
|
|
14
|
-
},
|
|
15
|
-
"supports": {
|
|
16
|
-
"reasoning": true,
|
|
17
|
-
"temperature": true,
|
|
18
|
-
"toolCall": true,
|
|
19
|
-
"structuredOutput": true,
|
|
20
|
-
"attachment": false
|
|
21
|
-
},
|
|
22
|
-
"effort": {
|
|
23
|
-
"levels": ["low", "high", "max"],
|
|
24
|
-
"default": "max",
|
|
25
|
-
"reserveShare": 0.12
|
|
26
|
-
},
|
|
27
|
-
"roles": {
|
|
28
|
-
"plan": "max",
|
|
29
|
-
"build": "high",
|
|
30
|
-
"reviewer": "max",
|
|
31
|
-
"locate": "low",
|
|
32
|
-
"verify": "low"
|
|
33
|
-
},
|
|
34
|
-
"prune": {
|
|
35
|
-
"enabled": true,
|
|
36
|
-
"keepRecentToolCalls": 4,
|
|
37
|
-
"keepRecentTurns": 2,
|
|
38
|
-
"keepUserMessages": true
|
|
39
|
-
},
|
|
40
|
-
"fallback": []
|
|
41
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$comment": "Split profile. The heavy reasoning roles run on Muse Spark 1.3 because it is the strongest free model on Zen by a wide margin (Terminal-Bench 2.1 88.8% vs Nemotron 3 Ultra's 56.4%, DeepSWE v1.1 75.4%). The bulk-output roles stay on Space Bunny because its 524,288-token output cap is 4x Muse Spark's 131,072, and scaffolding or migrating many files is where that ceiling is actually felt. `retention` below describes the BASE model only; the config header prints the effective retention, which is may-train because three roles run on Muse Spark.",
|
|
3
|
-
"id": "muse-split-free",
|
|
4
|
-
"title": "Muse Spark 1.3 (heavy) + Space Bunny Free (bulk output)",
|
|
5
|
-
"provider": "opencode",
|
|
6
|
-
"model": "space-bunny-free",
|
|
7
|
-
"family": null,
|
|
8
|
-
"verified": "2026-10-07",
|
|
9
|
-
"retention": "zero-retention",
|
|
10
|
-
"notes": "Opt-in split, not a default. Muse Spark is a Meta contributor-tier free endpoint: its provider may train on your prompts and completions. Three of the five roles are therefore may-train even though the base model is zero-retention. Install it only for public or throwaway repositories. Effort requests below are resolved against each role's own model, so plan and reviewer ask for `max` and land on `xhigh`, which is the top Muse Spark offers.",
|
|
11
|
-
"limits": {
|
|
12
|
-
"context": 1048576,
|
|
13
|
-
"input": 524288,
|
|
14
|
-
"output": 524288
|
|
15
|
-
},
|
|
16
|
-
"supports": {
|
|
17
|
-
"reasoning": true,
|
|
18
|
-
"temperature": false,
|
|
19
|
-
"toolCall": true,
|
|
20
|
-
"structuredOutput": false,
|
|
21
|
-
"attachment": true
|
|
22
|
-
},
|
|
23
|
-
"effort": {
|
|
24
|
-
"levels": ["low", "medium", "high", "xhigh", "max"],
|
|
25
|
-
"default": "max",
|
|
26
|
-
"reserveShare": 0.12
|
|
27
|
-
},
|
|
28
|
-
"roles": {
|
|
29
|
-
"plan": "max",
|
|
30
|
-
"build": "high",
|
|
31
|
-
"reviewer": "max",
|
|
32
|
-
"locate": "low",
|
|
33
|
-
"verify": "low"
|
|
34
|
-
},
|
|
35
|
-
"roleModels": {
|
|
36
|
-
"plan": {
|
|
37
|
-
"model": "muse-spark-1.3-contributor-free",
|
|
38
|
-
"levels": ["minimal", "low", "medium", "high", "xhigh"]
|
|
39
|
-
},
|
|
40
|
-
"build": {
|
|
41
|
-
"model": "muse-spark-1.3-contributor-free",
|
|
42
|
-
"levels": ["minimal", "low", "medium", "high", "xhigh"]
|
|
43
|
-
},
|
|
44
|
-
"reviewer": {
|
|
45
|
-
"model": "muse-spark-1.3-contributor-free",
|
|
46
|
-
"levels": ["minimal", "low", "medium", "high", "xhigh"]
|
|
47
|
-
},
|
|
48
|
-
"locate": {
|
|
49
|
-
"model": "space-bunny-free",
|
|
50
|
-
"levels": ["low", "medium", "high", "xhigh", "max"]
|
|
51
|
-
},
|
|
52
|
-
"verify": {
|
|
53
|
-
"model": "space-bunny-free",
|
|
54
|
-
"levels": ["low", "medium", "high", "xhigh", "max"]
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
"prune": {
|
|
58
|
-
"enabled": true,
|
|
59
|
-
"keepRecentToolCalls": 6,
|
|
60
|
-
"keepRecentTurns": 3,
|
|
61
|
-
"keepUserMessages": true
|
|
62
|
-
},
|
|
63
|
-
"fallback": ["x-preview-f-free", "longcat-2.5-preview-free", "deepseek-v4-flash-free"]
|
|
64
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$comment": "Validated against models.dev on 2026-10-07. Regenerate with `oc-harness use --refresh`.",
|
|
3
|
-
"id": "space-bunny-free",
|
|
4
|
-
"title": "Space Bunny Free (OpenCode Zen)",
|
|
5
|
-
"provider": "opencode",
|
|
6
|
-
"model": "space-bunny-free",
|
|
7
|
-
"family": null,
|
|
8
|
-
"verified": "2026-10-07",
|
|
9
|
-
"retention": "zero-retention",
|
|
10
|
-
"notes": "Stealth preview reasoning model. Served over /v1/chat/completions (openai-compatible), which is the weaker cache dialect compared with models on /v1/responses. At the time of writing it is the only free model on Zen whose top reasoning effort is `max` and whose provider is zero-retention. Free for a limited time; expect the slot to rotate.",
|
|
11
|
-
"limits": {
|
|
12
|
-
"context": 1048576,
|
|
13
|
-
"input": 524288,
|
|
14
|
-
"output": 524288
|
|
15
|
-
},
|
|
16
|
-
"supports": {
|
|
17
|
-
"reasoning": true,
|
|
18
|
-
"temperature": false,
|
|
19
|
-
"toolCall": true,
|
|
20
|
-
"structuredOutput": false,
|
|
21
|
-
"attachment": true
|
|
22
|
-
},
|
|
23
|
-
"effort": {
|
|
24
|
-
"levels": ["low", "medium", "high", "xhigh", "max"],
|
|
25
|
-
"default": "max",
|
|
26
|
-
"reserveShare": 0.12
|
|
27
|
-
},
|
|
28
|
-
"roles": {
|
|
29
|
-
"plan": "max",
|
|
30
|
-
"build": "max",
|
|
31
|
-
"reviewer": "max",
|
|
32
|
-
"locate": "low",
|
|
33
|
-
"verify": "low"
|
|
34
|
-
},
|
|
35
|
-
"prune": {
|
|
36
|
-
"enabled": true,
|
|
37
|
-
"keepRecentToolCalls": 6,
|
|
38
|
-
"keepRecentTurns": 3,
|
|
39
|
-
"keepUserMessages": true
|
|
40
|
-
},
|
|
41
|
-
"fallback": ["x-preview-f-free", "longcat-2.5-preview-free", "deepseek-v4-flash-free"]
|
|
42
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$comment": "Fallback tier 1 for space-bunny-free. Zero-retention, 1M context, and a `max` effort, but only three levels: there is no medium or xhigh, so roles asking for those resolve down to high.",
|
|
3
|
-
"id": "x-preview-f-free",
|
|
4
|
-
"title": "x-preview-f Free (OpenCode Zen)",
|
|
5
|
-
"provider": "opencode",
|
|
6
|
-
"model": "x-preview-f-free",
|
|
7
|
-
"family": null,
|
|
8
|
-
"verified": "2026-10-07",
|
|
9
|
-
"retention": "zero-retention",
|
|
10
|
-
"notes": "Only three effort levels (low, high, max) and a 131k completion ceiling, so long single answers are not its strength. Kept as first fallback: same 1M context class and the same zero-retention terms as the default.",
|
|
11
|
-
"limits": {
|
|
12
|
-
"context": 1000000,
|
|
13
|
-
"output": 131072
|
|
14
|
-
},
|
|
15
|
-
"supports": {
|
|
16
|
-
"reasoning": true,
|
|
17
|
-
"temperature": true,
|
|
18
|
-
"toolCall": true,
|
|
19
|
-
"structuredOutput": true,
|
|
20
|
-
"attachment": true
|
|
21
|
-
},
|
|
22
|
-
"effort": {
|
|
23
|
-
"levels": ["low", "high", "max"],
|
|
24
|
-
"default": "max",
|
|
25
|
-
"reserveShare": 0.12
|
|
26
|
-
},
|
|
27
|
-
"roles": {
|
|
28
|
-
"plan": "max",
|
|
29
|
-
"build": "high",
|
|
30
|
-
"reviewer": "max",
|
|
31
|
-
"locate": "low",
|
|
32
|
-
"verify": "low"
|
|
33
|
-
},
|
|
34
|
-
"prune": {
|
|
35
|
-
"enabled": true,
|
|
36
|
-
"keepRecentToolCalls": 6,
|
|
37
|
-
"keepRecentTurns": 3,
|
|
38
|
-
"keepUserMessages": true
|
|
39
|
-
},
|
|
40
|
-
"fallback": ["longcat-2.5-preview-free", "deepseek-v4-flash-free"]
|
|
41
|
-
}
|