@vizuh/sabi 0.1.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/README.md +40 -0
- package/mod/sabi.mjs +881 -0
- package/package.json +32 -0
- package/sabi.config.json +109 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @vizuh/sabi
|
|
2
|
+
|
|
3
|
+
Adaptive inference scheduling for [Command Code](https://commandcode.ai): a mod that plans each
|
|
4
|
+
continuing round — model and reasoning effort — from the trajectory's own state (tool calls and
|
|
5
|
+
their results, failure evidence, context size). Round 1 always runs on your session model; from
|
|
6
|
+
round 2 on, a read round goes cheap, edits and tests go mid, and a failing tool escalates.
|
|
7
|
+
|
|
8
|
+
This package is the **mod only**. The local proxy (BYOK, for any harness that accepts a `baseURL`)
|
|
9
|
+
lives in the [repository](https://github.com/vizuh/sabi).
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cmd mods add -g npm:@vizuh/sabi
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Update
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cmd mods update
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Uninstall
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cmd mods remove sabi
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
The package ships a default `sabi.config.json` next to the bundle, so it works with no setup.
|
|
32
|
+
To change tiers, policy or telemetry, put your own `sabi.config.json` in the project you run in,
|
|
33
|
+
or at `~/.config/sabi/sabi.config.json` — both take precedence over the shipped default (or set
|
|
34
|
+
`SABI_CONFIG` to point at one explicitly).
|
|
35
|
+
|
|
36
|
+
`harness.tiers` defaults to the strongest Command Code ids available from the Go plan up. A model
|
|
37
|
+
that is listed by `cmd --list-models` is not necessarily covered by your plan: an out-of-plan tier
|
|
38
|
+
answers `403 MODEL_NOT_IN_PLAN` and fails that round, so check the ids against your plan.
|
|
39
|
+
|
|
40
|
+
Docs, decisions and the full changelog: https://github.com/vizuh/sabi
|
package/mod/sabi.mjs
ADDED
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
// @vizuh/sabi 0.1.0 — generated by pack.mjs from packages/adapters/command-code/mod/sabi.ts.
|
|
2
|
+
// Source and docs: https://github.com/vizuh/sabi
|
|
3
|
+
|
|
4
|
+
// packages/core/src/state.ts
|
|
5
|
+
var EXPLORE_TOOLS = /* @__PURE__ */ new Set([
|
|
6
|
+
"read",
|
|
7
|
+
// OpenCode/Kilo
|
|
8
|
+
"webfetch",
|
|
9
|
+
"websearch",
|
|
10
|
+
"read_file",
|
|
11
|
+
"read_directory",
|
|
12
|
+
"glob",
|
|
13
|
+
"grep",
|
|
14
|
+
"web_search",
|
|
15
|
+
"web_fetch",
|
|
16
|
+
"shell_output",
|
|
17
|
+
"shell_tasks",
|
|
18
|
+
"get_diagnostics",
|
|
19
|
+
"todo_write",
|
|
20
|
+
"task_create",
|
|
21
|
+
"task_update",
|
|
22
|
+
"task_list",
|
|
23
|
+
"task_get",
|
|
24
|
+
"task_output",
|
|
25
|
+
"task_stop",
|
|
26
|
+
"sleep",
|
|
27
|
+
"cron_create",
|
|
28
|
+
"cron_list",
|
|
29
|
+
"cron_delete",
|
|
30
|
+
"schedule_wakeup",
|
|
31
|
+
"activate_skill"
|
|
32
|
+
]);
|
|
33
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set(["edit_file", "write_file", "edit", "write", "apply_patch"]);
|
|
34
|
+
var SHELL_TOOLS = /* @__PURE__ */ new Set(["shell_command", "bash"]);
|
|
35
|
+
var VERIFY_COMMAND = /\b(test|tests|vitest|jest|pytest|build|tsc|typecheck|type-check|lint|eslint|check)\b/i;
|
|
36
|
+
var EXPLORE_COMMAND = /(^|\s)(ls|cat|head|tail|find|rg|grep|wc|pwd)\b|\bgit\s+(status|diff|log|show|branch)\b/i;
|
|
37
|
+
var KIND_RANK = {
|
|
38
|
+
unclassified: 0,
|
|
39
|
+
exploration: 1,
|
|
40
|
+
implementation: 2,
|
|
41
|
+
verification: 3,
|
|
42
|
+
"first-turn": 4
|
|
43
|
+
};
|
|
44
|
+
var HARD_PATTERNS = [
|
|
45
|
+
{ re: /^\s*(?:Error|ERROR|error):/m, label: "error-line" },
|
|
46
|
+
{ re: /Traceback \(most recent call last\)/, label: "python-traceback" },
|
|
47
|
+
{ re: /\bpanic:/, label: "panic" },
|
|
48
|
+
{
|
|
49
|
+
re: /AssertionError|TypeError|ReferenceError|SyntaxError|ImportError|ModuleNotFoundError|Cannot find module/,
|
|
50
|
+
label: "exception"
|
|
51
|
+
},
|
|
52
|
+
{ re: /\berror TS\d+/, label: "typescript-error" },
|
|
53
|
+
{ re: /\b(?:FAIL|FAILED|FAILURES|Failing)\b/, label: "fail-marker" },
|
|
54
|
+
{ re: /\bcommand failed\b/i, label: "command-failed" },
|
|
55
|
+
{ re: /exit (?:code|status)[:=\s]+([1-9]\d*)/i, label: "nonzero-exit" },
|
|
56
|
+
{ re: /(\d+)\s+fail(?:ed|ing|ures)/i, label: "failure-count", numeric: true },
|
|
57
|
+
{ re: /command not found/i, label: "command-not-found" },
|
|
58
|
+
{ re: /permission denied/i, label: "permission-denied" },
|
|
59
|
+
{ re: /no such file or directory/i, label: "missing-file" }
|
|
60
|
+
];
|
|
61
|
+
var SOFT_PATTERNS = [
|
|
62
|
+
{ re: /warning/i, label: "soft-warning" },
|
|
63
|
+
{ re: /deprecated/i, label: "soft-deprecated" },
|
|
64
|
+
{ re: /retrying/i, label: "soft-retrying" },
|
|
65
|
+
{ re: /timed out/i, label: "soft-timeout" }
|
|
66
|
+
];
|
|
67
|
+
var TRANSPORT_LIMIT_PATTERNS = [
|
|
68
|
+
{ re: /rate[-_ ]?limit/i, label: "rate-limited" },
|
|
69
|
+
{ re: /too many requests/i, label: "rate-limited" },
|
|
70
|
+
{ re: /\b(?:session|usage|weekly|monthly|daily|hourly|subscription|plan)\s+limit\b/i, label: "quota-exceeded" },
|
|
71
|
+
{ re: /quota[- ]?exceeded/i, label: "quota-exceeded" },
|
|
72
|
+
{ re: /insufficient_quota|insufficient quota/i, label: "quota-exceeded" }
|
|
73
|
+
];
|
|
74
|
+
var TRANSPORT_PATTERNS = [
|
|
75
|
+
{ re: /\b429\b/, label: "rate-limited" },
|
|
76
|
+
{ re: /timed out|timeout/i, label: "timeout" }
|
|
77
|
+
];
|
|
78
|
+
var HARNESS_DENIAL = /\buser\b[^\n]{0,40}\b(?:denied|declined|rejected)\b|denied by (?:the )?user|\b(?:permission|tool call|request)[^\n]{0,30}\b(?:denied|declined|rejected)\b/i;
|
|
79
|
+
function textOf(content) {
|
|
80
|
+
if (content === null || content === void 0) return "";
|
|
81
|
+
if (typeof content === "string") return content;
|
|
82
|
+
if (typeof content === "number" || typeof content === "boolean") return String(content);
|
|
83
|
+
if (Array.isArray(content)) return content.map((part) => textOf(part)).join("\n");
|
|
84
|
+
if (typeof content === "object") {
|
|
85
|
+
const obj = content;
|
|
86
|
+
if (typeof obj.text === "string") return obj.text;
|
|
87
|
+
if (obj.content !== void 0) return textOf(obj.content);
|
|
88
|
+
if (typeof obj.output === "string") return obj.output;
|
|
89
|
+
}
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
var CHARS_PER_TOKEN = 3.6;
|
|
93
|
+
var MEDIA_TOKENS_PER_IMAGE = 1500;
|
|
94
|
+
var MEDIA_PART_TYPES = {
|
|
95
|
+
image: "image",
|
|
96
|
+
image_url: "image",
|
|
97
|
+
input_image: "image",
|
|
98
|
+
input_audio: "audio",
|
|
99
|
+
audio_url: "audio",
|
|
100
|
+
video_url: "video",
|
|
101
|
+
file: "file"
|
|
102
|
+
};
|
|
103
|
+
function payloadCharsOf(part) {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.stringify(part).length;
|
|
106
|
+
} catch {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function tallyMedia(content, tally, depth = 0) {
|
|
111
|
+
if (depth > 3 || !Array.isArray(content)) return;
|
|
112
|
+
for (const part of content) {
|
|
113
|
+
if (!part || typeof part !== "object") continue;
|
|
114
|
+
const record = part;
|
|
115
|
+
const kind = typeof record.type === "string" ? MEDIA_PART_TYPES[record.type] : void 0;
|
|
116
|
+
if (kind) {
|
|
117
|
+
tally.counts[kind] = (tally.counts[kind] ?? 0) + 1;
|
|
118
|
+
if (kind !== "image") tally.payloadChars += payloadCharsOf(record);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (record.content !== void 0) tallyMedia(record.content, tally, depth + 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function mediaTokens(tally) {
|
|
125
|
+
const images = tally.counts.image ?? 0;
|
|
126
|
+
return images * MEDIA_TOKENS_PER_IMAGE + Math.ceil(tally.payloadChars / CHARS_PER_TOKEN);
|
|
127
|
+
}
|
|
128
|
+
function modalitiesOf(counts) {
|
|
129
|
+
const kinds = Object.keys(counts).filter((kind) => (counts[kind] ?? 0) > 0).sort();
|
|
130
|
+
return ["text", ...kinds];
|
|
131
|
+
}
|
|
132
|
+
function isHarnessDenial(text) {
|
|
133
|
+
return HARNESS_DENIAL.test(text);
|
|
134
|
+
}
|
|
135
|
+
function detectFailure(texts) {
|
|
136
|
+
const evidence = [];
|
|
137
|
+
let hard = 0;
|
|
138
|
+
let soft = 0;
|
|
139
|
+
let transport = 0;
|
|
140
|
+
const note = (label) => {
|
|
141
|
+
if (evidence.length < 4) evidence.push(label);
|
|
142
|
+
};
|
|
143
|
+
for (const raw of texts) {
|
|
144
|
+
const text = String(raw ?? "");
|
|
145
|
+
if (!text.trim()) continue;
|
|
146
|
+
if (isHarnessDenial(text)) {
|
|
147
|
+
note("permission-denial");
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const namedLimit = TRANSPORT_LIMIT_PATTERNS.find((pattern) => pattern.re.test(text));
|
|
151
|
+
if (namedLimit) {
|
|
152
|
+
transport += 1;
|
|
153
|
+
note(namedLimit.label);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
let textHard = false;
|
|
157
|
+
for (const pattern of HARD_PATTERNS) {
|
|
158
|
+
const match = text.match(pattern.re);
|
|
159
|
+
if (!match) continue;
|
|
160
|
+
if (pattern.numeric) {
|
|
161
|
+
const count = Number((match[1] ?? "").replace(/\D/g, ""));
|
|
162
|
+
if (!(count > 0)) continue;
|
|
163
|
+
}
|
|
164
|
+
hard += 1;
|
|
165
|
+
textHard = true;
|
|
166
|
+
note(pattern.label);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (textHard) continue;
|
|
170
|
+
const statusSignal = TRANSPORT_PATTERNS.find((pattern) => pattern.re.test(text));
|
|
171
|
+
if (statusSignal) {
|
|
172
|
+
transport += 1;
|
|
173
|
+
note(statusSignal.label);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
for (const pattern of SOFT_PATTERNS) {
|
|
177
|
+
if (pattern.re.test(text)) {
|
|
178
|
+
soft += 1;
|
|
179
|
+
note(pattern.label);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const level = hard >= 1 ? "hard" : transport >= 1 ? "transport" : soft >= 2 ? "soft" : "none";
|
|
185
|
+
return { level, evidence };
|
|
186
|
+
}
|
|
187
|
+
var RTK_WRAPPERS = /* @__PURE__ */ new Set(["err", "test", "proxy", "summary"]);
|
|
188
|
+
var RTK_READ_VERBS = /* @__PURE__ */ new Set(["read", "smart", "json", "env", "log", "deps", "session", "gain", "discover", "recall"]);
|
|
189
|
+
function unwrapRtk(command) {
|
|
190
|
+
const parts = command.trim().split(/\s+/);
|
|
191
|
+
if ((parts[0] ?? "").toLowerCase() !== "rtk") return command.trim();
|
|
192
|
+
const rest = parts.slice(1);
|
|
193
|
+
if (rest.length > 1 && RTK_WRAPPERS.has((rest[0] ?? "").toLowerCase())) rest.shift();
|
|
194
|
+
return rest.join(" ");
|
|
195
|
+
}
|
|
196
|
+
function shellKind(command) {
|
|
197
|
+
if (!command) return "unclassified";
|
|
198
|
+
const wrapped = command.trim().toLowerCase().startsWith("rtk ");
|
|
199
|
+
const unwrapped = unwrapRtk(command);
|
|
200
|
+
const verb = (unwrapped.split(/\s+/)[0] ?? "").toLowerCase();
|
|
201
|
+
if (wrapped && RTK_READ_VERBS.has(verb)) return "exploration";
|
|
202
|
+
if (VERIFY_COMMAND.test(unwrapped)) return "verification";
|
|
203
|
+
if (EXPLORE_COMMAND.test(unwrapped)) return "exploration";
|
|
204
|
+
return "unclassified";
|
|
205
|
+
}
|
|
206
|
+
function callKind(name, args) {
|
|
207
|
+
if (EDIT_TOOLS.has(name)) return "implementation";
|
|
208
|
+
if (EXPLORE_TOOLS.has(name)) return "exploration";
|
|
209
|
+
if (SHELL_TOOLS.has(name)) {
|
|
210
|
+
let command;
|
|
211
|
+
if (args) {
|
|
212
|
+
try {
|
|
213
|
+
const parsed = JSON.parse(args);
|
|
214
|
+
if (typeof parsed.command === "string") command = parsed.command;
|
|
215
|
+
} catch {
|
|
216
|
+
command = void 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return shellKind(command);
|
|
220
|
+
}
|
|
221
|
+
return "unclassified";
|
|
222
|
+
}
|
|
223
|
+
function classifyRound(calls) {
|
|
224
|
+
if (!calls.length) return "unclassified";
|
|
225
|
+
let best = "unclassified";
|
|
226
|
+
for (const call of calls) {
|
|
227
|
+
const kind = callKind(call.name, call.args);
|
|
228
|
+
if (kind === "unclassified") return "unclassified";
|
|
229
|
+
if (KIND_RANK[kind] > KIND_RANK[best]) best = kind;
|
|
230
|
+
}
|
|
231
|
+
return best;
|
|
232
|
+
}
|
|
233
|
+
function transcriptStats(messages) {
|
|
234
|
+
let assistantTurns = 0;
|
|
235
|
+
let toolMessages = 0;
|
|
236
|
+
let contextChars = 0;
|
|
237
|
+
const media = { counts: {}, payloadChars: 0 };
|
|
238
|
+
const list = messages ?? [];
|
|
239
|
+
for (const raw of list) {
|
|
240
|
+
const message = raw;
|
|
241
|
+
const role = String(message?.role ?? "");
|
|
242
|
+
if (role === "assistant") assistantTurns += 1;
|
|
243
|
+
if (role === "tool") toolMessages += 1;
|
|
244
|
+
contextChars += textOf(message?.content).length;
|
|
245
|
+
if (message?.tool_calls) contextChars += JSON.stringify(message.tool_calls).length;
|
|
246
|
+
tallyMedia(message?.content, media);
|
|
247
|
+
}
|
|
248
|
+
return { messageCount: list.length, assistantTurns, toolMessages, contextChars, media };
|
|
249
|
+
}
|
|
250
|
+
function measuredContextTokens(usage) {
|
|
251
|
+
if (!usage) return void 0;
|
|
252
|
+
const total = usage.totalTokens ?? (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0);
|
|
253
|
+
return Number.isFinite(total) && total > 0 ? Math.floor(total) : void 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// packages/core/src/policy.ts
|
|
257
|
+
var POLICY_ORDER = [
|
|
258
|
+
"stuck",
|
|
259
|
+
"failure",
|
|
260
|
+
"context-pressure",
|
|
261
|
+
"transport",
|
|
262
|
+
"first-turn",
|
|
263
|
+
"verification",
|
|
264
|
+
"implementation",
|
|
265
|
+
"exploration",
|
|
266
|
+
"unclassified"
|
|
267
|
+
];
|
|
268
|
+
var REASONS = {
|
|
269
|
+
failure: (state) => `failure evidence: ${state.failureEvidence[0] ?? "unknown"}`,
|
|
270
|
+
stuck: (state) => `repeated failure (streak ${state.failureStreak ?? 2}); investigate instead of escalating more`,
|
|
271
|
+
"context-pressure": (state) => `context ${state.contextTokens ?? state.estimatedTokens} tokens vs window ${state.contextWindow ?? "unknown"}; prefer a big-window model`,
|
|
272
|
+
transport: (state) => `transport error (${state.failureEvidence[0] ?? "rate-limited"}), not a reasoning failure \u2014 retry, do not escalate`,
|
|
273
|
+
"first-turn": (state) => state.assistantTurns === 0 ? "new session, no prior turns" : "new user instruction",
|
|
274
|
+
verification: () => "verification round (tests/build/check)",
|
|
275
|
+
implementation: () => "implementation round (edits)",
|
|
276
|
+
exploration: () => "exploration round (read/search/bookkeeping)",
|
|
277
|
+
unclassified: () => "unclassified round"
|
|
278
|
+
};
|
|
279
|
+
function matches(condition, state) {
|
|
280
|
+
switch (condition) {
|
|
281
|
+
case "failure":
|
|
282
|
+
return state.failure === "hard";
|
|
283
|
+
case "stuck":
|
|
284
|
+
return state.repeatedFailure === true;
|
|
285
|
+
case "context-pressure":
|
|
286
|
+
return state.contextTokens !== void 0 && state.contextWindow !== void 0 && state.contextTokens > 0.9 * state.contextWindow;
|
|
287
|
+
case "transport":
|
|
288
|
+
return state.failure === "transport";
|
|
289
|
+
case "first-turn":
|
|
290
|
+
return state.roundKind === "first-turn";
|
|
291
|
+
case "verification":
|
|
292
|
+
return state.roundKind === "verification";
|
|
293
|
+
case "implementation":
|
|
294
|
+
return state.roundKind === "implementation";
|
|
295
|
+
case "exploration":
|
|
296
|
+
return state.roundKind === "exploration";
|
|
297
|
+
case "unclassified":
|
|
298
|
+
return state.roundKind === "unclassified";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function decideTier(state, policy, options = {}) {
|
|
302
|
+
const exclude = new Set(options.exclude ?? []);
|
|
303
|
+
for (const condition of POLICY_ORDER) {
|
|
304
|
+
if (exclude.has(condition)) continue;
|
|
305
|
+
if (condition === "stuck") {
|
|
306
|
+
if (state.repeatedFailure === true) {
|
|
307
|
+
const tier2 = options.stuckTier && options.stuckTier !== "off" ? options.stuckTier : policy.stuck;
|
|
308
|
+
if (tier2 && tier2 !== "off") {
|
|
309
|
+
return { rule: "stuck", tier: tier2, reason: REASONS.stuck(state) };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const tier = policy[condition];
|
|
315
|
+
if (!tier || tier === "off") continue;
|
|
316
|
+
if (matches(condition, state)) {
|
|
317
|
+
return { rule: condition, tier, reason: REASONS[condition](state) };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const fallback = policy.unclassified;
|
|
321
|
+
return {
|
|
322
|
+
rule: "fallback",
|
|
323
|
+
tier: fallback && fallback !== "off" ? fallback : "cheap",
|
|
324
|
+
reason: "no policy rule matched; fallback tier"
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// packages/core/src/compatibility.ts
|
|
329
|
+
function servesInputModalities(declared, required) {
|
|
330
|
+
if (!declared) return true;
|
|
331
|
+
return required.every((modality) => declared.includes(modality));
|
|
332
|
+
}
|
|
333
|
+
function firstServingTier(tiers, required, declared) {
|
|
334
|
+
for (const [name, tier] of Object.entries(tiers)) {
|
|
335
|
+
if (servesInputModalities(declared(tier), required)) return name;
|
|
336
|
+
}
|
|
337
|
+
return void 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// packages/core/src/config.ts
|
|
341
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
342
|
+
import os from "node:os";
|
|
343
|
+
import path from "node:path";
|
|
344
|
+
import { fileURLToPath } from "node:url";
|
|
345
|
+
var CONFIG_FILE = "sabi.config.json";
|
|
346
|
+
var PACKAGE_ROOT = fileURLToPath(new URL("../", import.meta.url));
|
|
347
|
+
function userConfigPath(env) {
|
|
348
|
+
const base = env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), ".config");
|
|
349
|
+
return path.join(base, "sabi", CONFIG_FILE);
|
|
350
|
+
}
|
|
351
|
+
function nearestConfigAbove(start) {
|
|
352
|
+
let dir = path.resolve(start);
|
|
353
|
+
for (; ; ) {
|
|
354
|
+
const candidate = path.join(dir, CONFIG_FILE);
|
|
355
|
+
if (existsSync(candidate)) return candidate;
|
|
356
|
+
const parent = path.dirname(dir);
|
|
357
|
+
if (parent === dir) return void 0;
|
|
358
|
+
dir = parent;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function configSearchPaths(options = {}) {
|
|
362
|
+
const env = options.env ?? process.env;
|
|
363
|
+
const explicit = env.SABI_CONFIG?.trim();
|
|
364
|
+
if (explicit) return [explicit];
|
|
365
|
+
const candidates = [
|
|
366
|
+
path.join(options.cwd ?? process.cwd(), CONFIG_FILE),
|
|
367
|
+
userConfigPath(env)
|
|
368
|
+
];
|
|
369
|
+
const above = nearestConfigAbove(options.packageRoot ?? PACKAGE_ROOT);
|
|
370
|
+
if (above) candidates.push(above);
|
|
371
|
+
return candidates;
|
|
372
|
+
}
|
|
373
|
+
function defaultConfigPath(options = {}) {
|
|
374
|
+
const candidates = configSearchPaths(options);
|
|
375
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
|
376
|
+
}
|
|
377
|
+
function loadConfig(configPath = defaultConfigPath()) {
|
|
378
|
+
let text;
|
|
379
|
+
try {
|
|
380
|
+
text = readFileSync(configPath, "utf8");
|
|
381
|
+
} catch {
|
|
382
|
+
const searched = configSearchPaths();
|
|
383
|
+
const hint = searched.length > 1 ? `searched: ${searched.join(", ")} \u2014 set SABI_CONFIG to point at one` : "set SABI_CONFIG to point at one";
|
|
384
|
+
throw new Error(`Sabi config not found at ${configPath} (${hint})`);
|
|
385
|
+
}
|
|
386
|
+
let parsed;
|
|
387
|
+
try {
|
|
388
|
+
parsed = JSON.parse(text);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
throw new Error(`Sabi config ${configPath} is not valid JSON: ${error.message}`);
|
|
391
|
+
}
|
|
392
|
+
return validateConfig(parsed, configPath);
|
|
393
|
+
}
|
|
394
|
+
var MODALITIES = ["text", "image", "audio", "video", "file"];
|
|
395
|
+
function isObject(value) {
|
|
396
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
397
|
+
}
|
|
398
|
+
function validateModelMetadata(value, label) {
|
|
399
|
+
if (!isObject(value)) throw new Error(`${label} must be an object`);
|
|
400
|
+
const fail = (field, expected) => {
|
|
401
|
+
throw new Error(`${label}.${field} ${expected}`);
|
|
402
|
+
};
|
|
403
|
+
const integer = (number, field, minimum = 1) => {
|
|
404
|
+
if (typeof number !== "number" || !Number.isSafeInteger(number) || number < minimum) {
|
|
405
|
+
fail(field, `must be a safe integer >= ${minimum}`);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
for (const field of ["contextWindow", "maxOutputTokens"]) {
|
|
409
|
+
if (value[field] !== void 0) integer(value[field], field);
|
|
410
|
+
}
|
|
411
|
+
if (typeof value.contextWindow === "number" && typeof value.maxOutputTokens === "number" && value.maxOutputTokens > value.contextWindow) {
|
|
412
|
+
fail("maxOutputTokens", "cannot exceed contextWindow");
|
|
413
|
+
}
|
|
414
|
+
if (value.capabilities !== void 0) {
|
|
415
|
+
if (!isObject(value.capabilities)) fail("capabilities", "must be an object");
|
|
416
|
+
const capabilities = value.capabilities;
|
|
417
|
+
const fields = [
|
|
418
|
+
"tools",
|
|
419
|
+
"parallelTools",
|
|
420
|
+
"strictTools",
|
|
421
|
+
"inputModalities",
|
|
422
|
+
"outputModalities",
|
|
423
|
+
"structuredOutput",
|
|
424
|
+
"reasoningEfforts",
|
|
425
|
+
"supportedParameters"
|
|
426
|
+
];
|
|
427
|
+
for (const field of Object.keys(capabilities)) {
|
|
428
|
+
if (!fields.includes(field)) fail(`capabilities.${field}`, "is not a supported metadata field");
|
|
429
|
+
}
|
|
430
|
+
for (const field of ["tools", "parallelTools", "strictTools"]) {
|
|
431
|
+
if (capabilities[field] !== void 0 && typeof capabilities[field] !== "boolean") {
|
|
432
|
+
fail(`capabilities.${field}`, "must be a boolean");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (capabilities.tools === false && (capabilities.parallelTools === true || capabilities.strictTools === true)) {
|
|
436
|
+
fail("capabilities.tools", "cannot be false when parallelTools or strictTools is true");
|
|
437
|
+
}
|
|
438
|
+
for (const field of ["inputModalities", "outputModalities", "structuredOutput", "reasoningEfforts", "supportedParameters"]) {
|
|
439
|
+
const items = capabilities[field];
|
|
440
|
+
if (items === void 0) continue;
|
|
441
|
+
if (!Array.isArray(items) || items.some((item) => typeof item !== "string" || !item.trim())) {
|
|
442
|
+
fail(`capabilities.${field}`, "must be an array of nonempty strings");
|
|
443
|
+
}
|
|
444
|
+
const strings = items;
|
|
445
|
+
if (new Set(strings).size !== strings.length) fail(`capabilities.${field}`, "must not contain duplicates");
|
|
446
|
+
const allowed = field.endsWith("Modalities") ? MODALITIES : field === "structuredOutput" ? ["json_object", "json_schema"] : void 0;
|
|
447
|
+
if (allowed && strings.some((item) => !allowed.includes(item))) {
|
|
448
|
+
fail(`capabilities.${field}`, `must contain only: ${allowed.join(", ")}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (value.contextAccounting !== void 0) {
|
|
453
|
+
if (!isObject(value.contextAccounting)) fail("contextAccounting", "must be an object");
|
|
454
|
+
const accounting = value.contextAccounting;
|
|
455
|
+
for (const field of Object.keys(accounting)) {
|
|
456
|
+
if (!["textTokensPerByte", "requestOverheadTokens", "perMessageOverheadTokens", "mediaTokens"].includes(field)) {
|
|
457
|
+
fail(`contextAccounting.${field}`, "is not a supported metadata field");
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (typeof accounting.textTokensPerByte !== "number" || !Number.isFinite(accounting.textTokensPerByte) || accounting.textTokensPerByte <= 0) {
|
|
461
|
+
fail("contextAccounting.textTokensPerByte", "must be a positive finite number");
|
|
462
|
+
}
|
|
463
|
+
integer(accounting.requestOverheadTokens, "contextAccounting.requestOverheadTokens", 0);
|
|
464
|
+
integer(accounting.perMessageOverheadTokens, "contextAccounting.perMessageOverheadTokens", 0);
|
|
465
|
+
if (accounting.mediaTokens !== void 0) {
|
|
466
|
+
if (!isObject(accounting.mediaTokens)) fail("contextAccounting.mediaTokens", "must be an object");
|
|
467
|
+
for (const [modality, tokens] of Object.entries(accounting.mediaTokens)) {
|
|
468
|
+
if (!MODALITIES.slice(1).includes(modality)) fail(`contextAccounting.mediaTokens.${modality}`, "is not a supported modality");
|
|
469
|
+
integer(tokens, `contextAccounting.mediaTokens.${modality}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (value.cost !== void 0) {
|
|
474
|
+
if (!isObject(value.cost)) fail("cost", "must be an object");
|
|
475
|
+
const rates = value.cost;
|
|
476
|
+
for (const field of ["input", "output", ...rates.cacheRead !== void 0 ? ["cacheRead"] : []]) {
|
|
477
|
+
if (typeof rates[field] !== "number" || !Number.isFinite(rates[field]) || rates[field] < 0) {
|
|
478
|
+
fail(`cost.${field}`, "must be a nonnegative finite number");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function validateConfig(value, source = "<inline>") {
|
|
484
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
485
|
+
throw new Error(`Sabi config ${source}: expected a JSON object`);
|
|
486
|
+
}
|
|
487
|
+
const config = value;
|
|
488
|
+
const upstreams = config.upstreams ?? {};
|
|
489
|
+
const models = config.models ?? {};
|
|
490
|
+
const aliases = config.aliases ?? {};
|
|
491
|
+
const policy = config.policy ?? {};
|
|
492
|
+
for (const [name, entries] of Object.entries({ upstreams, models, aliases, policy })) {
|
|
493
|
+
if (!isObject(entries)) throw new Error(`Sabi config ${source}: ${name} must be an object`);
|
|
494
|
+
}
|
|
495
|
+
if (config.compatibility !== void 0) {
|
|
496
|
+
if (!isObject(config.compatibility) || !["legacy", "strict"].includes(config.compatibility.mode)) {
|
|
497
|
+
throw new Error(`Sabi config ${source}: compatibility.mode must be 'legacy' or 'strict'`);
|
|
498
|
+
}
|
|
499
|
+
if (Object.keys(config.compatibility).some((field) => field !== "mode")) {
|
|
500
|
+
throw new Error(`Sabi config ${source}: compatibility has an unknown field`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (!Object.keys(upstreams).length) throw new Error(`Sabi config ${source}: no upstreams declared`);
|
|
504
|
+
for (const [name, upstream] of Object.entries(upstreams)) {
|
|
505
|
+
if (!upstream || typeof upstream.baseURL !== "string" || !upstream.baseURL) {
|
|
506
|
+
throw new Error(`Sabi config ${source}: upstream '${name}' has no baseURL`);
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
new URL(upstream.baseURL);
|
|
510
|
+
} catch {
|
|
511
|
+
throw new Error(`Sabi config ${source}: upstream '${name}' has an invalid baseURL`);
|
|
512
|
+
}
|
|
513
|
+
if (upstream.enabled !== void 0 && typeof upstream.enabled !== "boolean") {
|
|
514
|
+
throw new Error(`Sabi config ${source}: upstream '${name}'.enabled must be a boolean`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!Object.keys(models).length) throw new Error(`Sabi config ${source}: no models declared`);
|
|
518
|
+
for (const [name, model] of Object.entries(models)) {
|
|
519
|
+
validateModelMetadata(model, `Sabi config ${source}: model tier '${name}'`);
|
|
520
|
+
if (!model || typeof model.model !== "string" || !model.model) {
|
|
521
|
+
throw new Error(`Sabi config ${source}: model tier '${name}' has no model id`);
|
|
522
|
+
}
|
|
523
|
+
if (typeof model.upstream !== "string" || !Object.hasOwn(upstreams, model.upstream)) {
|
|
524
|
+
throw new Error(`Sabi config ${source}: model tier '${name}' references unknown upstream '${model.upstream}'`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (!Object.keys(aliases).length) throw new Error(`Sabi config ${source}: no aliases declared`);
|
|
528
|
+
for (const [alias, target] of Object.entries(aliases)) {
|
|
529
|
+
if (typeof target !== "string" || target !== "auto" && !Object.hasOwn(models, target)) {
|
|
530
|
+
throw new Error(`Sabi config ${source}: alias '${alias}' targets unknown tier '${target}'`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (const [condition, tier] of Object.entries(policy)) {
|
|
534
|
+
if (typeof tier !== "string" || tier !== "off" && !Object.hasOwn(models, tier)) {
|
|
535
|
+
throw new Error(`Sabi config ${source}: policy rule '${condition}' targets unknown tier '${tier}'`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const judge = config.judge;
|
|
539
|
+
if (judge !== void 0) {
|
|
540
|
+
if (typeof judge !== "object" || judge === null || typeof judge.enabled !== "boolean") {
|
|
541
|
+
throw new Error(`Sabi config ${source}: judge.enabled must be a boolean`);
|
|
542
|
+
}
|
|
543
|
+
if (judge.enabled) {
|
|
544
|
+
if (typeof judge.baseURL !== "string" || !judge.baseURL) {
|
|
545
|
+
throw new Error(`Sabi config ${source}: judge.baseURL is required when the judge is enabled`);
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
new URL(judge.baseURL);
|
|
549
|
+
} catch {
|
|
550
|
+
throw new Error(`Sabi config ${source}: judge.baseURL is not a valid URL`);
|
|
551
|
+
}
|
|
552
|
+
if (judge.model !== void 0 && typeof judge.model !== "string") {
|
|
553
|
+
throw new Error(`Sabi config ${source}: judge.model must be a string`);
|
|
554
|
+
}
|
|
555
|
+
if (judge.callOn !== void 0) {
|
|
556
|
+
if (!Array.isArray(judge.callOn) || judge.callOn.some((rule) => typeof rule !== "string")) {
|
|
557
|
+
throw new Error(`Sabi config ${source}: judge.callOn must be an array of policy rule names`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
for (const [name, value2] of Object.entries(judge.thresholds ?? {})) {
|
|
561
|
+
if (typeof value2 !== "number" || value2 < 0 || value2 > 1) {
|
|
562
|
+
throw new Error(`Sabi config ${source}: judge.thresholds.${name} must be a number between 0 and 1`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
for (const [name, value2] of [
|
|
566
|
+
["timeoutMs", judge.timeoutMs],
|
|
567
|
+
["cacheTtlMs", judge.cacheTtlMs],
|
|
568
|
+
["maxStateChars", judge.maxStateChars],
|
|
569
|
+
["costPerMTokInput", judge.costPerMTokInput]
|
|
570
|
+
]) {
|
|
571
|
+
if (value2 !== void 0 && (typeof value2 !== "number" || value2 <= 0)) {
|
|
572
|
+
throw new Error(`Sabi config ${source}: judge.${name} must be a positive number`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const telemetry = config.telemetry;
|
|
578
|
+
if (telemetry !== void 0) {
|
|
579
|
+
if (typeof telemetry !== "object" || telemetry === null) {
|
|
580
|
+
throw new Error(`Sabi config ${source}: telemetry must be an object`);
|
|
581
|
+
}
|
|
582
|
+
if (telemetry.allowlistOnly !== void 0 && typeof telemetry.allowlistOnly !== "boolean") {
|
|
583
|
+
throw new Error(`Sabi config ${source}: telemetry.allowlistOnly must be a boolean`);
|
|
584
|
+
}
|
|
585
|
+
if (telemetry.captureSnippets !== void 0 && typeof telemetry.captureSnippets !== "boolean") {
|
|
586
|
+
throw new Error(`Sabi config ${source}: telemetry.captureSnippets must be a boolean`);
|
|
587
|
+
}
|
|
588
|
+
if (telemetry.captureChars !== void 0 && (typeof telemetry.captureChars !== "number" || telemetry.captureChars <= 0)) {
|
|
589
|
+
throw new Error(`Sabi config ${source}: telemetry.captureChars must be a positive number`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const harness = config.harness;
|
|
593
|
+
if (harness !== void 0) {
|
|
594
|
+
if (typeof harness !== "object" || harness === null) {
|
|
595
|
+
throw new Error(`Sabi config ${source}: harness must be an object`);
|
|
596
|
+
}
|
|
597
|
+
if (!harness.tiers || typeof harness.tiers !== "object" || Array.isArray(harness.tiers)) {
|
|
598
|
+
throw new Error(`Sabi config ${source}: harness.tiers must be an object mapping tier names to models`);
|
|
599
|
+
}
|
|
600
|
+
for (const [tierName, tier] of Object.entries(harness.tiers)) {
|
|
601
|
+
if (!tier || typeof tier !== "object" || typeof tier.model !== "string" || !tier.model) {
|
|
602
|
+
throw new Error(`Sabi config ${source}: harness.tiers.${tierName} must declare a model id`);
|
|
603
|
+
}
|
|
604
|
+
if (tier.effort !== void 0 && typeof tier.effort !== "string") {
|
|
605
|
+
throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.effort must be a string`);
|
|
606
|
+
}
|
|
607
|
+
if (tier.minPlan !== void 0 && typeof tier.minPlan !== "string") {
|
|
608
|
+
throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.minPlan must be a string`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return { ...config, upstreams, models, aliases, policy };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// packages/core/src/harness.ts
|
|
616
|
+
function roundKindOf(round, calls) {
|
|
617
|
+
if (round.assistantTurns === 0 || round.lastRole === "user") return "first-turn";
|
|
618
|
+
if (round.lastRole === "tool") return classifyRound(calls);
|
|
619
|
+
return "unclassified";
|
|
620
|
+
}
|
|
621
|
+
function trajectoryFromRound(round, previous) {
|
|
622
|
+
const calls = round.calls.map((call) => ({ name: call.name, args: call.args }));
|
|
623
|
+
const outputs = round.calls.map((call) => call.output ?? "").filter((output) => output.trim().length > 0);
|
|
624
|
+
const detected = detectFailure(outputs);
|
|
625
|
+
const failed = round.calls.some((call) => call.failed === true);
|
|
626
|
+
const failure = failed ? detected.level === "transport" ? "transport" : "hard" : detected.level === "soft" ? "soft" : "none";
|
|
627
|
+
const failureEvidence = failed ? detected.evidence.length > 0 ? detected.evidence : ["tool-error"] : detected.evidence;
|
|
628
|
+
const contextTokens = round.contextTokens ?? Math.ceil(round.contextChars / CHARS_PER_TOKEN) + mediaTokens(roundMedia(round));
|
|
629
|
+
const sameFailure = previous?.failure === "hard" && failure === "hard";
|
|
630
|
+
const repeatedFailure = sameFailure === true;
|
|
631
|
+
const failureStreak = repeatedFailure ? 2 : failure === "hard" ? 1 : 0;
|
|
632
|
+
return {
|
|
633
|
+
messageCount: round.messageCount,
|
|
634
|
+
assistantTurns: round.assistantTurns,
|
|
635
|
+
toolMessages: round.calls.length,
|
|
636
|
+
lastRole: round.lastRole,
|
|
637
|
+
contextChars: round.contextChars,
|
|
638
|
+
estimatedTokens: Math.ceil(round.contextChars / CHARS_PER_TOKEN) + mediaTokens(roundMedia(round)),
|
|
639
|
+
contextTokens,
|
|
640
|
+
contextKnown: true,
|
|
641
|
+
hasTools: round.hasTools,
|
|
642
|
+
toolNames: round.toolNames,
|
|
643
|
+
lastToolNames: calls.map((call) => call.name),
|
|
644
|
+
roundKind: roundKindOf(round, calls),
|
|
645
|
+
failure,
|
|
646
|
+
failureEvidence,
|
|
647
|
+
repeatedFailure,
|
|
648
|
+
failureStreak,
|
|
649
|
+
...round.contextGeneration ? { contextGeneration: round.contextGeneration } : {},
|
|
650
|
+
...round.inputModalities ? { inputModalities: round.inputModalities } : {},
|
|
651
|
+
...round.mediaCounts && Object.keys(round.mediaCounts).length ? { inputModalities: round.inputModalities ?? modalitiesOf(round.mediaCounts), mediaCounts: round.mediaCounts } : {}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function roundMedia(round) {
|
|
655
|
+
return { counts: round.mediaCounts ?? {}, payloadChars: 0 };
|
|
656
|
+
}
|
|
657
|
+
function planRound(state, policy, tiers, options = {}) {
|
|
658
|
+
const withWindow = options.contextWindow !== void 0 && state.contextWindow === void 0 ? { ...state, contextWindow: options.contextWindow } : state;
|
|
659
|
+
const decision = decideTier(withWindow, policy, { stuckTier: policy.stuck });
|
|
660
|
+
const required = withWindow.inputModalities ?? [];
|
|
661
|
+
let tier = decision.tier;
|
|
662
|
+
let rule = decision.rule;
|
|
663
|
+
let reason = decision.reason;
|
|
664
|
+
const planned = tiers[tier];
|
|
665
|
+
if (planned && !servesInputModalities(planned.inputModalities, required)) {
|
|
666
|
+
const alternate = firstServingTier(tiers, required, (entry) => entry.inputModalities);
|
|
667
|
+
if (!alternate) return void 0;
|
|
668
|
+
reason = `input needs ${required.join("+")}; '${tier}' (${planned.model}) cannot accept it, '${alternate}' can`;
|
|
669
|
+
rule = "capability";
|
|
670
|
+
tier = alternate;
|
|
671
|
+
}
|
|
672
|
+
const chosen = tiers[tier];
|
|
673
|
+
if (!chosen || !chosen.model) return void 0;
|
|
674
|
+
return {
|
|
675
|
+
tier,
|
|
676
|
+
model: chosen.model,
|
|
677
|
+
effort: chosen.effort,
|
|
678
|
+
rule,
|
|
679
|
+
reason,
|
|
680
|
+
state: withWindow
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// packages/core/src/telemetry.ts
|
|
685
|
+
var ALLOWLIST = /* @__PURE__ */ new Set([
|
|
686
|
+
"error-line",
|
|
687
|
+
"python-traceback",
|
|
688
|
+
"panic",
|
|
689
|
+
"exception",
|
|
690
|
+
"typescript-error",
|
|
691
|
+
"fail-marker",
|
|
692
|
+
"command-failed",
|
|
693
|
+
"nonzero-exit",
|
|
694
|
+
"failure-count",
|
|
695
|
+
"command-not-found",
|
|
696
|
+
"permission-denied",
|
|
697
|
+
"missing-file",
|
|
698
|
+
"soft-warning",
|
|
699
|
+
"soft-deprecated",
|
|
700
|
+
"soft-retrying",
|
|
701
|
+
"soft-timeout",
|
|
702
|
+
"tool-error",
|
|
703
|
+
"permission-denial",
|
|
704
|
+
"rate-limited",
|
|
705
|
+
"quota-exceeded",
|
|
706
|
+
"timeout"
|
|
707
|
+
]);
|
|
708
|
+
function allowlisted(value) {
|
|
709
|
+
const code = value.split(":")[0]?.trim();
|
|
710
|
+
return ALLOWLIST.has(code);
|
|
711
|
+
}
|
|
712
|
+
function telemetryPolicy(config) {
|
|
713
|
+
const captureSnippets = config?.captureSnippets === true;
|
|
714
|
+
const captureChars = config?.captureChars ?? 800;
|
|
715
|
+
return {
|
|
716
|
+
captureSnippets,
|
|
717
|
+
allowlisted: (value) => allowlisted(value),
|
|
718
|
+
snippet: (value) => captureSnippets ? String(value).slice(0, captureChars) : ""
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function sanitizeReason(reason, policy) {
|
|
722
|
+
if (!policy.captureSnippets) {
|
|
723
|
+
if (!reason || allowlisted(reason)) return reason;
|
|
724
|
+
const segments = reason.split(":").map((segment) => segment.trim());
|
|
725
|
+
const last = segments[segments.length - 1] ?? "";
|
|
726
|
+
if (allowlisted(last)) return reason;
|
|
727
|
+
return "reason withheld (telemetry.allowlistOnly)";
|
|
728
|
+
}
|
|
729
|
+
return policy.snippet(reason);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// packages/core/src/prompt.ts
|
|
733
|
+
import * as readline from "node:readline/promises";
|
|
734
|
+
|
|
735
|
+
// packages/adapters/command-code/mod/sabi.ts
|
|
736
|
+
var MOD_ID = "sabi";
|
|
737
|
+
var DECISION_TYPE = "sabi/decision";
|
|
738
|
+
function readLedger(state) {
|
|
739
|
+
const raw = state.modState?.[MOD_ID] ?? {};
|
|
740
|
+
return {
|
|
741
|
+
rounds: typeof raw.rounds === "number" ? raw.rounds : 0,
|
|
742
|
+
messageCount: typeof raw.messageCount === "number" ? raw.messageCount : 0,
|
|
743
|
+
contextChars: typeof raw.contextChars === "number" ? raw.contextChars : 0,
|
|
744
|
+
contextTokens: typeof raw.contextTokens === "number" ? raw.contextTokens : void 0,
|
|
745
|
+
generation: typeof raw.generation === "number" ? raw.generation : void 0,
|
|
746
|
+
toolNames: Array.isArray(raw.toolNames) ? raw.toolNames : [],
|
|
747
|
+
hasTools: raw.hasTools === true,
|
|
748
|
+
lastModel: raw.lastModel,
|
|
749
|
+
lastUsage: raw.lastUsage
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function writeLedger(state, ledger) {
|
|
753
|
+
return { ...state, modState: { ...state.modState, [MOD_ID]: ledger } };
|
|
754
|
+
}
|
|
755
|
+
function compactedSince(ledger, stats) {
|
|
756
|
+
return ledger.messageCount > 0 && stats.messageCount > 0 && stats.messageCount < ledger.messageCount;
|
|
757
|
+
}
|
|
758
|
+
function sabi(cmd) {
|
|
759
|
+
let config;
|
|
760
|
+
try {
|
|
761
|
+
config = loadConfig();
|
|
762
|
+
} catch (error) {
|
|
763
|
+
cmd.ui.notify(`Sabi disabled: ${error.message}`);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const tiers = config.harness?.tiers ?? {};
|
|
767
|
+
if (Object.keys(tiers).length === 0) {
|
|
768
|
+
cmd.ui.notify("Sabi disabled: sabi.config.json declares no harness.tiers");
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const policy = config.policy ?? {};
|
|
772
|
+
const telemetry = telemetryPolicy(config.telemetry);
|
|
773
|
+
let calls = [];
|
|
774
|
+
let nextPlan;
|
|
775
|
+
let servingPlan;
|
|
776
|
+
let servedBy;
|
|
777
|
+
let previousFailure;
|
|
778
|
+
cmd.on("model_request_end", (event) => {
|
|
779
|
+
if (typeof event.model === "string") servedBy = event.model;
|
|
780
|
+
});
|
|
781
|
+
cmd.hooks({
|
|
782
|
+
onTurnStart: ({ state, turnNumber }) => {
|
|
783
|
+
calls = [];
|
|
784
|
+
servingPlan = nextPlan;
|
|
785
|
+
nextPlan = void 0;
|
|
786
|
+
servedBy = void 0;
|
|
787
|
+
const ledger = readLedger(state);
|
|
788
|
+
return writeLedger(state, { ...ledger, rounds: turnNumber });
|
|
789
|
+
},
|
|
790
|
+
// Sabi observes tool outcomes and never rewrites what the model sees.
|
|
791
|
+
afterToolCall: ({ toolName, input, isError, result }) => {
|
|
792
|
+
calls.push({
|
|
793
|
+
name: toolName,
|
|
794
|
+
args: JSON.stringify(input ?? {}),
|
|
795
|
+
failed: isError === true,
|
|
796
|
+
output: typeof result === "string" ? result : void 0
|
|
797
|
+
});
|
|
798
|
+
return void 0;
|
|
799
|
+
},
|
|
800
|
+
prepareNextTurn: ({ state }) => {
|
|
801
|
+
const ledger = readLedger(state);
|
|
802
|
+
const stats = transcriptStats(state.messages);
|
|
803
|
+
if (compactedSince(ledger, stats)) {
|
|
804
|
+
previousFailure = void 0;
|
|
805
|
+
ledger.generation = (ledger.generation ?? 0) + 1;
|
|
806
|
+
ledger.contextTokens = void 0;
|
|
807
|
+
}
|
|
808
|
+
const round = {
|
|
809
|
+
messageCount: stats.messageCount,
|
|
810
|
+
assistantTurns: ledger.rounds,
|
|
811
|
+
lastRole: calls.length > 0 ? "tool" : "assistant",
|
|
812
|
+
contextChars: stats.contextChars,
|
|
813
|
+
contextTokens: ledger.contextTokens,
|
|
814
|
+
hasTools: ledger.hasTools,
|
|
815
|
+
toolNames: ledger.toolNames,
|
|
816
|
+
calls,
|
|
817
|
+
...ledger.generation ? { contextGeneration: ledger.generation } : {},
|
|
818
|
+
...Object.keys(stats.media.counts).length ? { inputModalities: modalitiesOf(stats.media.counts), mediaCounts: stats.media.counts } : {}
|
|
819
|
+
};
|
|
820
|
+
const trajectory = trajectoryFromRound(round, previousFailure);
|
|
821
|
+
const plan = planRound(trajectory, policy, tiers, { contextWindow: config.harness?.contextWindow });
|
|
822
|
+
if (!plan) return void 0;
|
|
823
|
+
nextPlan = plan;
|
|
824
|
+
return plan.effort ? { model: plan.model, effort: plan.effort } : { model: plan.model };
|
|
825
|
+
},
|
|
826
|
+
onTurnEnd: ({ state, turnNumber, usage }, ctx) => {
|
|
827
|
+
const ledger = readLedger(state);
|
|
828
|
+
const stats = transcriptStats(state.messages);
|
|
829
|
+
const compacted = compactedSince(ledger, stats);
|
|
830
|
+
const usedThisTurn = usage !== void 0;
|
|
831
|
+
const measured = usedThisTurn ? measuredContextTokens({ promptTokens: usage.inputTokens, completionTokens: usage.outputTokens }) : void 0;
|
|
832
|
+
const generation = (ledger.generation ?? 0) + (compacted ? 1 : 0);
|
|
833
|
+
const adopted = servingPlan ?? nextPlan;
|
|
834
|
+
const next = {
|
|
835
|
+
...ledger,
|
|
836
|
+
rounds: turnNumber,
|
|
837
|
+
messageCount: stats.messageCount,
|
|
838
|
+
contextChars: stats.contextChars,
|
|
839
|
+
// A billed total is measured, so it floors the next round. Without usage the previous
|
|
840
|
+
// floor stands — the transcript only grows — except across a rewrite, where the old
|
|
841
|
+
// size described a context the host has removed.
|
|
842
|
+
contextTokens: measured ?? (compacted ? void 0 : ledger.contextTokens),
|
|
843
|
+
...generation > 0 ? { generation } : {},
|
|
844
|
+
// Only advance attribution when a fresh value actually arrived this turn. A missing
|
|
845
|
+
// usage or model event stays unknown rather than re-serializing an old round's value.
|
|
846
|
+
lastModel: servedBy,
|
|
847
|
+
lastUsage: usedThisTurn ? usage : void 0
|
|
848
|
+
};
|
|
849
|
+
previousFailure = adopted ? { failure: adopted.state.failure, failureEvidence: adopted.state.failureEvidence } : void 0;
|
|
850
|
+
recordDecision(ctx, {
|
|
851
|
+
turn: turnNumber,
|
|
852
|
+
planned: servingPlan ? {
|
|
853
|
+
tier: servingPlan.tier,
|
|
854
|
+
model: servingPlan.model,
|
|
855
|
+
effort: servingPlan.effort,
|
|
856
|
+
rule: servingPlan.rule,
|
|
857
|
+
reason: sanitizeReason(String(servingPlan.reason ?? ""), telemetry),
|
|
858
|
+
roundKind: servingPlan.state.roundKind,
|
|
859
|
+
failure: servingPlan.state.failure,
|
|
860
|
+
evidence: servingPlan.state.failureEvidence.slice(0, 3),
|
|
861
|
+
repeatedFailure: servingPlan.state.repeatedFailure,
|
|
862
|
+
failureStreak: servingPlan.state.failureStreak,
|
|
863
|
+
contextTokens: servingPlan.state.contextTokens,
|
|
864
|
+
contextGeneration: servingPlan.state.contextGeneration,
|
|
865
|
+
inputModalities: servingPlan.state.inputModalities
|
|
866
|
+
} : void 0,
|
|
867
|
+
servedBy: servedBy ?? void 0,
|
|
868
|
+
usage: usedThisTurn ? usage : void 0,
|
|
869
|
+
// Decision records never embed raw tool output by default; snippet capture is opt-in.
|
|
870
|
+
captureSnippets: telemetry.captureSnippets
|
|
871
|
+
});
|
|
872
|
+
return writeLedger(state, next);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
function recordDecision(ctx, data) {
|
|
877
|
+
ctx?.session?.appendCustomEntry({ customType: DECISION_TYPE, data });
|
|
878
|
+
}
|
|
879
|
+
export {
|
|
880
|
+
sabi as default
|
|
881
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vizuh/sabi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Adaptive inference scheduling for Command Code: one bundled mod that routes each continuing round by model, effort and trajectory state.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/vizuh/sabi.git",
|
|
9
|
+
"directory": "packages/adapters/command-code"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/vizuh/sabi#readme",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"commandcode",
|
|
14
|
+
"command-code",
|
|
15
|
+
"mod",
|
|
16
|
+
"llm",
|
|
17
|
+
"routing"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"commandcode": {
|
|
23
|
+
"mods": [
|
|
24
|
+
"./mod/sabi.mjs"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"mod/sabi.mjs",
|
|
29
|
+
"sabi.config.json",
|
|
30
|
+
"README.md"
|
|
31
|
+
]
|
|
32
|
+
}
|
package/sabi.config.json
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provenance": "Model ids, context windows and prices verified live from https://openrouter.ai/api/v1/models on 2026-09-18; input modalities for the same ids verified from the same endpoint the same day (`architecture.input_modalities`). Prices are USD per 1M tokens. Declared modalities are enforced: an undeclared capability is unknown, a declared one is binding.",
|
|
3
|
+
"server": { "host": "127.0.0.1", "port": 8787 },
|
|
4
|
+
"upstreams": {
|
|
5
|
+
"openrouter": {
|
|
6
|
+
"baseURL": "https://openrouter.ai/api/v1",
|
|
7
|
+
"apiKey": "$OPENROUTER_API_KEY",
|
|
8
|
+
"streamUsage": true,
|
|
9
|
+
"headers": {
|
|
10
|
+
"HTTP-Referer": "https://github.com/vizuh/sabi",
|
|
11
|
+
"X-Title": "Sabi"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"ollama": {
|
|
15
|
+
"baseURL": "http://127.0.0.1:11434/v1",
|
|
16
|
+
"apiKey": false,
|
|
17
|
+
"streamUsage": false
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"models": {
|
|
21
|
+
"cheap": {
|
|
22
|
+
"upstream": "openrouter",
|
|
23
|
+
"model": "deepseek/deepseek-v4-flash-0731",
|
|
24
|
+
"contextWindow": 1310720,
|
|
25
|
+
"capabilities": { "inputModalities": ["text"] },
|
|
26
|
+
"cost": { "input": 0.06, "output": 0.12, "cacheRead": 0.012 }
|
|
27
|
+
},
|
|
28
|
+
"mid": {
|
|
29
|
+
"upstream": "openrouter",
|
|
30
|
+
"model": "openai/gpt-5.6-luna",
|
|
31
|
+
"contextWindow": 1050000,
|
|
32
|
+
"capabilities": { "inputModalities": ["text", "image", "file"] },
|
|
33
|
+
"cost": { "input": 0.2, "output": 1.2, "cacheRead": 0.02 }
|
|
34
|
+
},
|
|
35
|
+
"strong": {
|
|
36
|
+
"upstream": "openrouter",
|
|
37
|
+
"model": "anthropic/claude-sonnet-5",
|
|
38
|
+
"contextWindow": 1000000,
|
|
39
|
+
"capabilities": { "inputModalities": ["text", "image", "file"] },
|
|
40
|
+
"cost": { "input": 2, "output": 10, "cacheRead": 0.2 }
|
|
41
|
+
},
|
|
42
|
+
"local": {
|
|
43
|
+
"upstream": "ollama",
|
|
44
|
+
"model": "qwen2.5-coder:7b",
|
|
45
|
+
"contextWindow": 32768,
|
|
46
|
+
"capabilities": { "inputModalities": ["text"] },
|
|
47
|
+
"cost": { "input": 0, "output": 0 }
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"aliases": {
|
|
51
|
+
"sabi-code": "auto",
|
|
52
|
+
"sabi-cheap": "cheap",
|
|
53
|
+
"sabi-mid": "mid",
|
|
54
|
+
"sabi-strong": "strong",
|
|
55
|
+
"sabi-local": "local"
|
|
56
|
+
},
|
|
57
|
+
"policy": {
|
|
58
|
+
"failure": "strong",
|
|
59
|
+
"stuck": "mid",
|
|
60
|
+
"context-pressure": "mid",
|
|
61
|
+
"transport": "mid",
|
|
62
|
+
"first-turn": "mid",
|
|
63
|
+
"verification": "mid",
|
|
64
|
+
"implementation": "mid",
|
|
65
|
+
"exploration": "cheap",
|
|
66
|
+
"unclassified": "cheap"
|
|
67
|
+
},
|
|
68
|
+
"telemetry": {
|
|
69
|
+
"allowlistOnly": true,
|
|
70
|
+
"captureSnippets": false,
|
|
71
|
+
"captureChars": 800
|
|
72
|
+
},
|
|
73
|
+
"judge": {
|
|
74
|
+
"enabled": true,
|
|
75
|
+
"baseURL": "https://api.typesafe.ai/v1",
|
|
76
|
+
"apiKey": "$TYPESAFE_API_KEY",
|
|
77
|
+
"model": "jev-latest",
|
|
78
|
+
"timeoutMs": 2500,
|
|
79
|
+
"cacheTtlMs": 600000,
|
|
80
|
+
"callOn": ["failure", "unclassified"],
|
|
81
|
+
"thresholds": { "realProblem": 0.6, "veto": 0.25, "difficultyConfidence": 0.6 },
|
|
82
|
+
"maxStateChars": 6000,
|
|
83
|
+
"costPerMTokInput": 0.042
|
|
84
|
+
},
|
|
85
|
+
"harness": {
|
|
86
|
+
"provenance": "Command Code catalog ids, efforts and min plans verified 2026-09-18 against `cmd --list-models` and the bundled reference models.md. Used by the in-process mod adapter (harness keeps its own loop, no proxy, no key). The `models` tiers above are the separate BYOK proxy path and are unused while the mod is active. A tier must be a model the account can actually serve: `cmd --list-models` prints the whole catalog regardless of plan, and an out-of-plan model answers 403 MODEL_NOT_IN_PLAN and fails that round. The defaults are the strongest ids available from the Go plan up; docs/install.md lists Pro and Max presets. `contextWindow` is the largest verified window among the tiers (1M), used by the context-pressure rule. `inputModalities` mirror the CLI's own model registry, read from the shipped bundle on 2026-09-18: plain `deepseek-v4-flash` and `glm-5.3` are text-only while `gpt-5.6-luna` accepts images. The host strips images for a text-only model, so a tier that cannot read them is passed over for one that can.",
|
|
87
|
+
"tiers": {
|
|
88
|
+
"cheap": {
|
|
89
|
+
"model": "deepseek/deepseek-v4-flash",
|
|
90
|
+
"effort": "high",
|
|
91
|
+
"minPlan": "Go",
|
|
92
|
+
"inputModalities": ["text"]
|
|
93
|
+
},
|
|
94
|
+
"mid": {
|
|
95
|
+
"model": "gpt-5.6-luna",
|
|
96
|
+
"effort": "high",
|
|
97
|
+
"minPlan": "Go",
|
|
98
|
+
"inputModalities": ["text", "image"]
|
|
99
|
+
},
|
|
100
|
+
"strong": {
|
|
101
|
+
"model": "zai-org/glm-5.3",
|
|
102
|
+
"effort": "high",
|
|
103
|
+
"minPlan": "Go",
|
|
104
|
+
"inputModalities": ["text"]
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"contextWindow": 1000000
|
|
108
|
+
}
|
|
109
|
+
}
|