@vizuh/sabi 0.1.5 → 0.2.1
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 +25 -8
- package/mod/sabi.mjs +184 -12
- package/package.json +14 -5
- package/sabi.config.json +17 -15
package/README.md
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
# @vizuh/sabi: Command Code
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
# @vizuh/sabi: Sabi inference adapter (Command Code mod)
|
|
2
|
+
|
|
3
|
+
Sabi is harness-independent adaptive inference scheduling for coding-agent trajectories:
|
|
4
|
+
per-round routing of model, effort and provider from trajectory evidence, while each host
|
|
5
|
+
harness keeps its own loop. Sabi promotes no single harness; every host below is a
|
|
6
|
+
peer surface with its own install path.
|
|
7
|
+
|
|
8
|
+
This npm artifact is the Command Code mod — one Sabi surface, not the whole product.
|
|
9
|
+
See the [adapter directory](../../../docs/adapters/README.md) for the product map and
|
|
10
|
+
evidence boundaries.
|
|
11
|
+
|
|
12
|
+
| Harness | Sabi surface | Install |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| Claude Code | Controller hooks (task/session, fail-open) | `npm install --global @vizuh/sabi-controller` then `sabi setup` |
|
|
15
|
+
| Codex | Controller hooks (task/session, fail-open) | `npm install --global @vizuh/sabi-controller` then `sabi setup` |
|
|
16
|
+
| Command Code | This package: in-process mod, per-round model + reasoning effort | `cmd mods add -g npm:@vizuh/sabi` |
|
|
17
|
+
| Hermes | Local proxy + native `llm_request` middleware | Checkout-based setup in [the install guide](../../../docs/install.md) |
|
|
18
|
+
| Oh My Pi | Local proxy via OpenAI-compatible extension provider | Checkout: `omp --extension packages/adapters/oh-my-pi/src/sabi-extension.mjs --model sabi/sabi-code` |
|
|
19
|
+
| OpenCode | Local proxy, plus optional controller hook | Checkout-based proxy (`npm start` + `npm run connect:opencode`); hooks via `@vizuh/sabi-controller` |
|
|
20
|
+
| Orca | Controller plugin + inventory/dispatch bridge | `npm install --global @vizuh/sabi-controller` then `sabi setup` |
|
|
21
|
+
| DeepSeek Harness, Kilo, Cline, Prime Agent | Proxy or bundle paths | See the [adapter directory](../../../docs/adapters/README.md) |
|
|
7
22
|
|
|
8
23
|
Adaptive inference scheduling for [Command Code](https://commandcode.ai): a mod that plans each
|
|
9
24
|
continuing round, including model and reasoning effort, from the trajectory's own state (tool calls and
|
|
@@ -17,8 +32,10 @@ The two paths are independent:
|
|
|
17
32
|
|
|
18
33
|
- The Command Code mod needs no Sabi provider key or proxy. It routes the subscription already
|
|
19
34
|
available to Command Code.
|
|
20
|
-
- The local proxy works with
|
|
21
|
-
OpenRouter, Ollama, or another configured upstream.
|
|
35
|
+
- The local proxy works with Hermes, Oh My Pi, OpenCode, and other OpenAI-compatible clients (including Cline, Kilo and Prime Agent paths). It uses
|
|
36
|
+
OpenRouter, Ollama, or another configured upstream. In the shipped default the OpenRouter
|
|
37
|
+
upstream is **free-models-only** (`paidModelsAllowed: false`): a priced model id is refused
|
|
38
|
+
before the request leaves the process, so the proxy cannot spend on its own.
|
|
22
39
|
|
|
23
40
|
For the proxy, Sabi loads only the credential names referenced by `sabi.config.json`. Existing
|
|
24
41
|
environment variables win, followed by `SABI_SECRETS_FILE`, the nearest workspace `secrets/.env`,
|
package/mod/sabi.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @vizuh/sabi 0.1
|
|
1
|
+
// @vizuh/sabi 0.2.1 — generated by pack.mjs from packages/adapters/command-code/mod/sabi.ts.
|
|
2
2
|
// Source and docs: https://github.com/vizuh/sabi
|
|
3
3
|
|
|
4
4
|
// packages/core/src/telemetry.ts
|
|
@@ -163,8 +163,8 @@ function normalizeTrajectoryEvidence(value, fallbackGeneration = 0) {
|
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
165
|
function boundTrajectoryEvidence(values, maxItems = MAX_EVIDENCE_ITEMS) {
|
|
166
|
-
const
|
|
167
|
-
if (maxItems <= 0) return
|
|
166
|
+
const result2 = [];
|
|
167
|
+
if (maxItems <= 0) return result2;
|
|
168
168
|
const seen = /* @__PURE__ */ new Set();
|
|
169
169
|
for (const value of values ?? []) {
|
|
170
170
|
const item = normalizeTrajectoryEvidence(value);
|
|
@@ -172,10 +172,10 @@ function boundTrajectoryEvidence(values, maxItems = MAX_EVIDENCE_ITEMS) {
|
|
|
172
172
|
const key = JSON.stringify(item);
|
|
173
173
|
if (seen.has(key)) continue;
|
|
174
174
|
seen.add(key);
|
|
175
|
-
|
|
176
|
-
if (
|
|
175
|
+
result2.push(item);
|
|
176
|
+
if (result2.length >= Math.max(0, Math.min(maxItems, MAX_EVIDENCE_ITEMS))) break;
|
|
177
177
|
}
|
|
178
|
-
return
|
|
178
|
+
return result2;
|
|
179
179
|
}
|
|
180
180
|
function countScope(value) {
|
|
181
181
|
if (typeof value === "number") return Number.isSafeInteger(value) && value >= 0 ? { count: value } : {};
|
|
@@ -323,7 +323,11 @@ var TRANSPORT_LIMIT_PATTERNS = [
|
|
|
323
323
|
{ re: /too many requests/i, label: "rate-limited" },
|
|
324
324
|
{ re: /\b(?:session|usage|weekly|monthly|daily|hourly|subscription|plan)\s+limit\b/i, label: "quota-exceeded" },
|
|
325
325
|
{ re: /quota[- ]?exceeded/i, label: "quota-exceeded" },
|
|
326
|
-
{ re: /insufficient_quota|insufficient quota/i, label: "quota-exceeded" }
|
|
326
|
+
{ re: /insufficient_quota|insufficient quota/i, label: "quota-exceeded" },
|
|
327
|
+
// A dropped connection is the transport failing. Without this, the wording a client prints after
|
|
328
|
+
// a transport deadline reads as a task failure and the next round escalates straight back to the
|
|
329
|
+
// tier that just timed out — ten consecutive 120s rounds in one real session.
|
|
330
|
+
{ re: /socket (?:connection )?(?:was )?closed|socket hang up|ECONNRESET|other side closed|connection reset/i, label: "connection-closed" }
|
|
327
331
|
];
|
|
328
332
|
var TRANSPORT_PATTERNS = [
|
|
329
333
|
{ re: /\b429\b/, label: "rate-limited" },
|
|
@@ -591,6 +595,96 @@ function firstServingTier(tiers, required, declared) {
|
|
|
591
595
|
return void 0;
|
|
592
596
|
}
|
|
593
597
|
|
|
598
|
+
// packages/core/src/cache-routing.ts
|
|
599
|
+
var SWITCH_ACTIONS = /* @__PURE__ */ new Set([
|
|
600
|
+
"escalate-model",
|
|
601
|
+
"fresh-context",
|
|
602
|
+
"rollback-with-reflection",
|
|
603
|
+
"retry-with-feedback"
|
|
604
|
+
]);
|
|
605
|
+
function finiteNonNegative(value) {
|
|
606
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
607
|
+
}
|
|
608
|
+
function boundedTokens(value) {
|
|
609
|
+
return Number.isSafeInteger(value) && finiteNonNegative(value) ? value : void 0;
|
|
610
|
+
}
|
|
611
|
+
function cacheObservationFromUsage(usage) {
|
|
612
|
+
if (!usage || !Number.isSafeInteger(usage.promptTokens) || usage.promptTokens < 0 || !Number.isSafeInteger(usage.cachedTokens) || usage.cachedTokens < 0) {
|
|
613
|
+
return { status: "unknown" };
|
|
614
|
+
}
|
|
615
|
+
const cachedTokens = Math.min(usage.promptTokens, usage.cachedTokens);
|
|
616
|
+
return {
|
|
617
|
+
status: cachedTokens > 0 ? "hit" : "miss",
|
|
618
|
+
promptTokens: usage.promptTokens,
|
|
619
|
+
cachedTokens
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
function phaseOf(input, sameToolCycle) {
|
|
623
|
+
if (input.state.failure === "hard") return "failure";
|
|
624
|
+
if (input.recoveryAction && SWITCH_ACTIONS.has(input.recoveryAction)) return "escalation";
|
|
625
|
+
if (sameToolCycle) return "same-tool-cycle";
|
|
626
|
+
if (input.previousTier !== void 0) return "new-phase";
|
|
627
|
+
return "unknown";
|
|
628
|
+
}
|
|
629
|
+
function routeCost(rate, tokens) {
|
|
630
|
+
return finiteNonNegative(rate) && boundedTokens(tokens) !== void 0 ? rate * tokens / 1e6 : void 0;
|
|
631
|
+
}
|
|
632
|
+
function switchEconomics(input, cachedTokens, contextTokens) {
|
|
633
|
+
const expectedGain = input.previousCost && input.plannedCost && contextTokens !== void 0 ? routeCost(Math.max(0, input.previousCost.input - input.plannedCost.input), contextTokens) : void 0;
|
|
634
|
+
const cachePenalty = input.previousCost && input.plannedCost && cachedTokens !== void 0 ? routeCost(Math.max(0, input.plannedCost.input - (input.previousCost.cacheRead ?? input.previousCost.input)), cachedTokens) : void 0;
|
|
635
|
+
return {
|
|
636
|
+
...expectedGain !== void 0 ? { expectedGain } : {},
|
|
637
|
+
...cachePenalty !== void 0 ? { cachePenalty } : {}
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function result(input, action, phase, selectedTier, reason, extra = {}) {
|
|
641
|
+
const cache = input.previousCache;
|
|
642
|
+
const contextTokens = boundedTokens(input.state.contextTokens) ?? boundedTokens(input.state.estimatedTokens);
|
|
643
|
+
const cachedTokens = boundedTokens(cache?.cachedTokens);
|
|
644
|
+
const reprocessTokens = cache?.status === "hit" && cachedTokens !== void 0 ? cachedTokens : void 0;
|
|
645
|
+
return {
|
|
646
|
+
action,
|
|
647
|
+
phase,
|
|
648
|
+
cacheStatus: cache?.status ?? "unknown",
|
|
649
|
+
plannedTier: input.plannedTier,
|
|
650
|
+
selectedTier,
|
|
651
|
+
...input.previousTier !== void 0 ? { previousTier: input.previousTier } : {},
|
|
652
|
+
...contextTokens !== void 0 ? { estimatedContextTokens: contextTokens } : {},
|
|
653
|
+
...cachedTokens !== void 0 ? { cachedTokens } : {},
|
|
654
|
+
...reprocessTokens !== void 0 ? { reprocessTokens } : {},
|
|
655
|
+
...extra,
|
|
656
|
+
reason
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function cacheAwareRoute(input) {
|
|
660
|
+
const sameToolCycle = input.state.lastRole === "tool" && input.previousTier !== void 0 && (input.state.contextGeneration ?? 0) === (input.previousGeneration ?? 0);
|
|
661
|
+
const phase = phaseOf(input, sameToolCycle);
|
|
662
|
+
const canKeep = input.previousTier !== void 0 && input.canKeepPrevious !== false;
|
|
663
|
+
const cache = input.previousCache;
|
|
664
|
+
const cachedTokens = boundedTokens(cache?.cachedTokens);
|
|
665
|
+
const contextTokens = boundedTokens(input.state.contextTokens) ?? boundedTokens(input.state.estimatedTokens);
|
|
666
|
+
const economics = switchEconomics(input, cachedTokens, contextTokens);
|
|
667
|
+
if (!input.previousTier || !canKeep) {
|
|
668
|
+
return result(input, "evaluate", phase, input.plannedTier, "no usable previous route affinity; policy decision evaluated", economics);
|
|
669
|
+
}
|
|
670
|
+
if (input.plannedTier === input.previousTier) {
|
|
671
|
+
return result(input, "evaluate", phase, input.previousTier, "policy selected the current model; route unchanged", economics);
|
|
672
|
+
}
|
|
673
|
+
if (phase === "same-tool-cycle") {
|
|
674
|
+
return result(input, "keep", phase, input.previousTier, "same tool cycle; keep the current model", economics);
|
|
675
|
+
}
|
|
676
|
+
if (phase === "failure" || phase === "escalation") {
|
|
677
|
+
return result(input, "switch", phase, input.plannedTier, "failure or escalation requires evaluating a different model", economics);
|
|
678
|
+
}
|
|
679
|
+
if (cache?.status === "hit" && cachedTokens !== void 0 && economics.expectedGain !== void 0 && economics.cachePenalty !== void 0 && economics.expectedGain > economics.cachePenalty) {
|
|
680
|
+
return result(input, "switch", phase, input.plannedTier, "expected cost gain exceeds the measured cache penalty", economics);
|
|
681
|
+
}
|
|
682
|
+
if (cache?.status === "hit" && cachedTokens !== void 0) {
|
|
683
|
+
return result(input, "keep", phase, input.previousTier, "cache hit retained; unpriced policy gain does not exceed cache loss", economics);
|
|
684
|
+
}
|
|
685
|
+
return result(input, "switch", phase, input.plannedTier, "policy changed phase without a measured cache hit to preserve", economics);
|
|
686
|
+
}
|
|
687
|
+
|
|
594
688
|
// packages/core/src/config.ts
|
|
595
689
|
import { existsSync, readFileSync } from "node:fs";
|
|
596
690
|
import os from "node:os";
|
|
@@ -770,6 +864,15 @@ function validateConfig(value, source = "<inline>") {
|
|
|
770
864
|
if (upstream.enabled !== void 0 && typeof upstream.enabled !== "boolean") {
|
|
771
865
|
throw new Error(`Sabi config ${source}: upstream '${name}'.enabled must be a boolean`);
|
|
772
866
|
}
|
|
867
|
+
if (upstream.paidModelsAllowed !== void 0 && typeof upstream.paidModelsAllowed !== "boolean") {
|
|
868
|
+
throw new Error(`Sabi config ${source}: upstream '${name}'.paidModelsAllowed must be a boolean`);
|
|
869
|
+
}
|
|
870
|
+
if (upstream.auth !== void 0 && upstream.auth !== "passthrough") {
|
|
871
|
+
throw new Error(`Sabi config ${source}: upstream '${name}'.auth must be 'passthrough'`);
|
|
872
|
+
}
|
|
873
|
+
if (upstream.auth === "passthrough" && upstream.apiKey !== void 0 && upstream.apiKey !== false) {
|
|
874
|
+
throw new Error(`Sabi config ${source}: upstream '${name}' is auth:passthrough and must not declare apiKey`);
|
|
875
|
+
}
|
|
773
876
|
}
|
|
774
877
|
if (!Object.keys(models).length) throw new Error(`Sabi config ${source}: no models declared`);
|
|
775
878
|
for (const [name, model] of Object.entries(models)) {
|
|
@@ -811,6 +914,21 @@ function validateConfig(value, source = "<inline>") {
|
|
|
811
914
|
throw new Error(`Sabi config ${source}: transportFallback.enabled must be a boolean`);
|
|
812
915
|
}
|
|
813
916
|
}
|
|
917
|
+
const passthrough = config.passthrough;
|
|
918
|
+
if (passthrough !== void 0) {
|
|
919
|
+
if (!isObject(passthrough)) throw new Error(`Sabi config ${source}: passthrough must be an object`);
|
|
920
|
+
for (const field of Object.keys(passthrough)) {
|
|
921
|
+
if (field !== "alias") throw new Error(`Sabi config ${source}: passthrough.${field} is not a supported field`);
|
|
922
|
+
}
|
|
923
|
+
if (passthrough.alias !== void 0) {
|
|
924
|
+
if (typeof passthrough.alias !== "string" || !passthrough.alias.trim()) {
|
|
925
|
+
throw new Error(`Sabi config ${source}: passthrough.alias must be a nonempty string`);
|
|
926
|
+
}
|
|
927
|
+
if (aliases[passthrough.alias] !== "auto") {
|
|
928
|
+
throw new Error(`Sabi config ${source}: passthrough.alias '${passthrough.alias}' must name an alias targeting 'auto'`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
814
932
|
const judge = config.judge;
|
|
815
933
|
if (judge !== void 0) {
|
|
816
934
|
if (typeof judge !== "object" || judge === null || typeof judge.enabled !== "boolean") {
|
|
@@ -1115,7 +1233,11 @@ function sanitizeDecisionRecord(record, config) {
|
|
|
1115
1233
|
upstreamModel: record.upstreamModel,
|
|
1116
1234
|
stream: record.stream,
|
|
1117
1235
|
state,
|
|
1236
|
+
cache: record.cache,
|
|
1118
1237
|
judge: record.judge,
|
|
1238
|
+
// Effort labels are short provider-declared strings, never transcript text; still bounded.
|
|
1239
|
+
...typeof record.effort === "string" && record.effort.trim() ? { effort: record.effort.trim().slice(0, 64) } : {},
|
|
1240
|
+
...record.effortSource === "client" || record.effortSource === "scheduled" || record.effortSource === "unspecified" ? { effortSource: record.effortSource } : {},
|
|
1119
1241
|
usage: record.usage,
|
|
1120
1242
|
cost: record.cost,
|
|
1121
1243
|
latencyMs: record.latencyMs,
|
|
@@ -1211,6 +1333,20 @@ function planRound(state, policy, tiers, options = {}) {
|
|
|
1211
1333
|
rule = "capability";
|
|
1212
1334
|
tier = alternate;
|
|
1213
1335
|
}
|
|
1336
|
+
const cache = cacheAwareRoute({
|
|
1337
|
+
state: withWindow,
|
|
1338
|
+
plannedTier: tier,
|
|
1339
|
+
previousTier: options.previous?.tier,
|
|
1340
|
+
previousLastRole: options.previous?.lastRole,
|
|
1341
|
+
previousGeneration: options.previous?.generation,
|
|
1342
|
+
previousCache: options.previous?.cache,
|
|
1343
|
+
canKeepPrevious: Boolean(options.previous?.tier && tiers[options.previous.tier] && servesInputModalities(tiers[options.previous.tier]?.inputModalities, required))
|
|
1344
|
+
});
|
|
1345
|
+
if (cache.selectedTier !== tier) {
|
|
1346
|
+
tier = cache.selectedTier;
|
|
1347
|
+
rule = "cache-affinity";
|
|
1348
|
+
reason = cache.reason;
|
|
1349
|
+
}
|
|
1214
1350
|
const chosen = tiers[tier];
|
|
1215
1351
|
if (!chosen || !chosen.model) return void 0;
|
|
1216
1352
|
return {
|
|
@@ -1219,13 +1355,31 @@ function planRound(state, policy, tiers, options = {}) {
|
|
|
1219
1355
|
effort: chosen.effort,
|
|
1220
1356
|
rule,
|
|
1221
1357
|
reason,
|
|
1222
|
-
state: withWindow
|
|
1358
|
+
state: withWindow,
|
|
1359
|
+
cache
|
|
1223
1360
|
};
|
|
1224
1361
|
}
|
|
1225
1362
|
|
|
1226
1363
|
// packages/core/src/prompt.ts
|
|
1227
1364
|
import * as readline from "node:readline/promises";
|
|
1228
1365
|
|
|
1366
|
+
// packages/core/src/signals.ts
|
|
1367
|
+
var KNOWN_SIGNAL_KINDS = [
|
|
1368
|
+
"failure.real",
|
|
1369
|
+
"failure.transport",
|
|
1370
|
+
"progress.stalled",
|
|
1371
|
+
"verification.complete",
|
|
1372
|
+
"coverage",
|
|
1373
|
+
"context.pressure",
|
|
1374
|
+
"context.staleness",
|
|
1375
|
+
"task.ambiguity",
|
|
1376
|
+
"mutation.risk",
|
|
1377
|
+
"retry.value",
|
|
1378
|
+
"evidence.nextValue",
|
|
1379
|
+
"model.requiredStrength"
|
|
1380
|
+
];
|
|
1381
|
+
var KNOWN_KINDS = new Set(KNOWN_SIGNAL_KINDS);
|
|
1382
|
+
|
|
1229
1383
|
// packages/adapters/command-code/mod/sabi.ts
|
|
1230
1384
|
import path3 from "node:path";
|
|
1231
1385
|
var MOD_ID = "sabi";
|
|
@@ -1243,6 +1397,9 @@ function readLedger(state) {
|
|
|
1243
1397
|
hasTools: raw.hasTools === true,
|
|
1244
1398
|
lastModel: raw.lastModel,
|
|
1245
1399
|
lastUsage: raw.lastUsage,
|
|
1400
|
+
lastTier: raw.lastTier,
|
|
1401
|
+
lastLastRole: raw.lastLastRole,
|
|
1402
|
+
lastCache: raw.lastCache,
|
|
1246
1403
|
sessionId: typeof raw.sessionId === "string" ? raw.sessionId : void 0
|
|
1247
1404
|
};
|
|
1248
1405
|
}
|
|
@@ -1311,12 +1468,12 @@ function sabi(cmd) {
|
|
|
1311
1468
|
return writeLedger(state, { ...ledger, rounds: turnNumber });
|
|
1312
1469
|
},
|
|
1313
1470
|
// Sabi observes tool outcomes and never rewrites what the model sees.
|
|
1314
|
-
afterToolCall: ({ toolName, input, isError, result }) => {
|
|
1471
|
+
afterToolCall: ({ toolName, input, isError, result: result2 }) => {
|
|
1315
1472
|
calls.push({
|
|
1316
1473
|
name: toolName,
|
|
1317
1474
|
args: JSON.stringify(input ?? {}),
|
|
1318
1475
|
failed: isError === true,
|
|
1319
|
-
output: typeof
|
|
1476
|
+
output: typeof result2 === "string" ? result2 : void 0
|
|
1320
1477
|
});
|
|
1321
1478
|
return void 0;
|
|
1322
1479
|
},
|
|
@@ -1327,6 +1484,9 @@ function sabi(cmd) {
|
|
|
1327
1484
|
previousFailure = void 0;
|
|
1328
1485
|
ledger.generation = (ledger.generation ?? 0) + 1;
|
|
1329
1486
|
ledger.contextTokens = void 0;
|
|
1487
|
+
ledger.lastTier = void 0;
|
|
1488
|
+
ledger.lastLastRole = void 0;
|
|
1489
|
+
ledger.lastCache = { status: "unknown" };
|
|
1330
1490
|
}
|
|
1331
1491
|
const round = {
|
|
1332
1492
|
messageCount: stats.messageCount,
|
|
@@ -1341,7 +1501,15 @@ function sabi(cmd) {
|
|
|
1341
1501
|
...Object.keys(stats.media.counts).length ? { inputModalities: modalitiesOf(stats.media.counts), mediaCounts: stats.media.counts } : {}
|
|
1342
1502
|
};
|
|
1343
1503
|
const trajectory = trajectoryFromRound(round, previousFailure);
|
|
1344
|
-
const plan = planRound(trajectory, policy, tiers, {
|
|
1504
|
+
const plan = planRound(trajectory, policy, tiers, {
|
|
1505
|
+
contextWindow: config.harness?.contextWindow,
|
|
1506
|
+
previous: {
|
|
1507
|
+
tier: ledger.lastTier,
|
|
1508
|
+
lastRole: ledger.lastLastRole,
|
|
1509
|
+
generation: ledger.generation,
|
|
1510
|
+
cache: ledger.lastCache
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1345
1513
|
if (!plan) return void 0;
|
|
1346
1514
|
nextPlan = plan;
|
|
1347
1515
|
return plan.effort ? { model: plan.model, effort: plan.effort } : { model: plan.model };
|
|
@@ -1367,7 +1535,10 @@ function sabi(cmd) {
|
|
|
1367
1535
|
// Only advance attribution when a fresh value actually arrived this turn. A missing
|
|
1368
1536
|
// usage or model event stays unknown rather than re-serializing an old round's value.
|
|
1369
1537
|
lastModel: servedBy,
|
|
1370
|
-
lastUsage: usedThisTurn ? usage : void 0
|
|
1538
|
+
lastUsage: usedThisTurn ? usage : void 0,
|
|
1539
|
+
lastTier: adopted?.tier ?? (compacted ? void 0 : ledger.lastTier),
|
|
1540
|
+
lastLastRole: adopted?.state.lastRole ?? (compacted ? void 0 : ledger.lastLastRole),
|
|
1541
|
+
lastCache: usedThisTurn ? cacheObservationFromUsage(toUsageTotals(usage)) : { status: "unknown" }
|
|
1371
1542
|
};
|
|
1372
1543
|
previousFailure = adopted ? { failure: adopted.state.failure, failureEvidence: adopted.state.failureEvidence } : void 0;
|
|
1373
1544
|
recordCustomEntry(ctx, {
|
|
@@ -1410,6 +1581,7 @@ function sabi(cmd) {
|
|
|
1410
1581
|
// This is the host adapter, not a provider entitlement claim.
|
|
1411
1582
|
upstream: CLIENT_ID,
|
|
1412
1583
|
upstreamModel: servingPlan.model,
|
|
1584
|
+
cache: servingPlan.cache,
|
|
1413
1585
|
stream: false,
|
|
1414
1586
|
state: {
|
|
1415
1587
|
...servingPlan.state,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizuh/sabi",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Sabi inference adapter for Command Code, part of harness-independent adaptive routing for Claude Code, Codex, Command Code, Hermes, Oh My Pi, OpenCode and Orca.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -11,11 +11,20 @@
|
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://github.com/vizuh/sabi#readme",
|
|
13
13
|
"keywords": [
|
|
14
|
-
"
|
|
14
|
+
"sabi",
|
|
15
|
+
"ai-agents",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"codex",
|
|
15
18
|
"command-code",
|
|
19
|
+
"commandcode",
|
|
20
|
+
"hermes",
|
|
21
|
+
"inference-scheduling",
|
|
22
|
+
"llm-routing",
|
|
16
23
|
"mod",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
24
|
+
"oh-my-pi",
|
|
25
|
+
"opencode",
|
|
26
|
+
"openai-compatible",
|
|
27
|
+
"orca"
|
|
19
28
|
],
|
|
20
29
|
"publishConfig": {
|
|
21
30
|
"access": "public"
|
package/sabi.config.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"provenance": "
|
|
2
|
+
"provenance": "Operator rule (2026-09-22): OpenRouter may serve FREE models only. The openrouter upstream declares paidModelsAllowed:false, so any model whose price is unknown or non-zero is refused at dispatch (packages/core/src/compatibility.ts) — a config mistake cannot spend money. Every openrouter model id below carries the :free variant and cost 0. Ids, context windows, output ceilings, modalities and zero pricing verified live from https://openrouter.ai/api/v1/models and a real completion per id on 2026-09-22. Jev (TypeSafe judge) is a separate upstream and unaffected by this rule. Operator rule (2026-09-23): Claude Code and Codex are used through their own subscriptions — Sabi installs hooks into them and never writes a provider base URL, an API key or a model override into either harness, so neither can become per-token spend through Sabi. Guarded by packages/controller/test/hooks.test.ts.",
|
|
3
3
|
"server": { "host": "127.0.0.1", "port": 8787 },
|
|
4
4
|
"upstreams": {
|
|
5
5
|
"openrouter": {
|
|
6
6
|
"baseURL": "https://openrouter.ai/api/v1",
|
|
7
7
|
"apiKey": "$OPENROUTER_API_KEY",
|
|
8
8
|
"streamUsage": true,
|
|
9
|
+
"paidModelsAllowed": false,
|
|
9
10
|
"headers": {
|
|
10
11
|
"HTTP-Referer": "https://github.com/vizuh/sabi",
|
|
11
12
|
"X-Title": "Sabi"
|
|
@@ -20,32 +21,33 @@
|
|
|
20
21
|
"models": {
|
|
21
22
|
"cheap": {
|
|
22
23
|
"upstream": "openrouter",
|
|
23
|
-
"model": "
|
|
24
|
-
"contextWindow":
|
|
25
|
-
"maxOutputTokens":
|
|
24
|
+
"model": "poolside/laguna-s-2.1:free",
|
|
25
|
+
"contextWindow": 262144,
|
|
26
|
+
"maxOutputTokens": 32768,
|
|
26
27
|
"capabilities": { "inputModalities": ["text"] },
|
|
27
|
-
"cost": { "input": 0
|
|
28
|
+
"cost": { "input": 0, "output": 0 }
|
|
28
29
|
},
|
|
29
30
|
"mid": {
|
|
30
31
|
"upstream": "openrouter",
|
|
31
|
-
"model": "
|
|
32
|
-
"contextWindow":
|
|
33
|
-
"maxOutputTokens":
|
|
34
|
-
"capabilities": { "inputModalities": ["text", "image"
|
|
35
|
-
"cost": { "input": 0
|
|
32
|
+
"model": "dots-studio/dots-3-note-preview:free",
|
|
33
|
+
"contextWindow": 512000,
|
|
34
|
+
"maxOutputTokens": 460800,
|
|
35
|
+
"capabilities": { "inputModalities": ["text", "image"] },
|
|
36
|
+
"cost": { "input": 0, "output": 0 }
|
|
36
37
|
},
|
|
37
38
|
"strong": {
|
|
38
39
|
"upstream": "openrouter",
|
|
39
|
-
"model": "
|
|
40
|
+
"model": "nvidia/nemotron-3-ultra-550b-a55b:free",
|
|
40
41
|
"contextWindow": 1000000,
|
|
41
|
-
"maxOutputTokens":
|
|
42
|
-
"capabilities": { "inputModalities": ["text"
|
|
43
|
-
"cost": { "input":
|
|
42
|
+
"maxOutputTokens": 65536,
|
|
43
|
+
"capabilities": { "inputModalities": ["text"] },
|
|
44
|
+
"cost": { "input": 0, "output": 0 }
|
|
44
45
|
},
|
|
45
46
|
"local": {
|
|
46
47
|
"upstream": "ollama",
|
|
47
48
|
"model": "qwen2.5-coder:7b",
|
|
48
49
|
"contextWindow": 32768,
|
|
50
|
+
"maxOutputTokens": 32768,
|
|
49
51
|
"capabilities": { "inputModalities": ["text"] },
|
|
50
52
|
"cost": { "input": 0, "output": 0 }
|
|
51
53
|
}
|
|
@@ -96,7 +98,7 @@
|
|
|
96
98
|
"costPerMTokInput": 0.042
|
|
97
99
|
},
|
|
98
100
|
"harness": {
|
|
99
|
-
"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).
|
|
101
|
+
"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). This is the Command Code subscription catalog, not OpenRouter — the free-models-only rule above does not apply to it.",
|
|
100
102
|
"tiers": {
|
|
101
103
|
"cheap": {
|
|
102
104
|
"model": "deepseek/deepseek-v4-flash",
|