@vizuh/sabi 0.1.2 → 0.1.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/README.md +14 -8
- package/mod/sabi.mjs +115 -4
- package/package.json +1 -1
- package/sabi.config.json +11 -1
package/README.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
# @vizuh/sabi
|
|
1
|
+
# @vizuh/sabi: Command Code adapter
|
|
2
|
+
|
|
3
|
+
This npm artifact is one Sabi adapter, not the whole Sabi product. Sabi also supports a local
|
|
4
|
+
OpenAI-compatible proxy for OpenCode, Hermes, Prime Agent, Kilo and other clients, plus an
|
|
5
|
+
experimental controller surface for Claude Code, Codex and Orca. See the
|
|
6
|
+
[adapter directory](../../../docs/adapters/README.md) for the product map and evidence boundaries.
|
|
2
7
|
|
|
3
8
|
Adaptive inference scheduling for [Command Code](https://commandcode.ai): a mod that plans each
|
|
4
|
-
continuing round
|
|
9
|
+
continuing round, including model and reasoning effort, from the trajectory's own state (tool calls and
|
|
5
10
|
their results, failure evidence, context size). Round 1 always runs on your session model; from
|
|
6
11
|
round 2 on, a read round goes cheap, edits and tests go mid, and a failing tool escalates.
|
|
7
12
|
|
|
@@ -10,15 +15,16 @@ lives in the [repository](https://github.com/vizuh/sabi).
|
|
|
10
15
|
|
|
11
16
|
The two paths are independent:
|
|
12
17
|
|
|
13
|
-
-
|
|
18
|
+
- The Command Code mod needs no Sabi provider key or proxy. It routes the subscription already
|
|
14
19
|
available to Command Code.
|
|
15
|
-
-
|
|
16
|
-
OpenRouter, Ollama or another configured upstream.
|
|
20
|
+
- The local proxy works with OpenCode, Hermes, Kilo, and other OpenAI-compatible clients. It uses
|
|
21
|
+
OpenRouter, Ollama, or another configured upstream.
|
|
17
22
|
|
|
18
23
|
For the proxy, Sabi loads only the credential names referenced by `sabi.config.json`. Existing
|
|
19
24
|
environment variables win, followed by `SABI_SECRETS_FILE`, the nearest workspace `secrets/.env`,
|
|
20
|
-
and `~/.config/sabi/secrets.env` or `~/.config/sabi/.env`. It
|
|
21
|
-
|
|
25
|
+
and `~/.config/sabi/secrets.env` or `~/.config/sabi/.env`. It does not copy loaded values into generated harness configuration or logs. If you use a workspace
|
|
26
|
+
secrets/.env, that source file is already in the worktree: keep it out of version control, add it to
|
|
27
|
+
.gitignore, and protect its file permissions. Users who do not use Jev can set `judge.enabled` to
|
|
22
28
|
`false`; users without a central secrets file can keep exporting provider variables normally.
|
|
23
29
|
|
|
24
30
|
## Install
|
|
@@ -43,7 +49,7 @@ cmd mods remove sabi
|
|
|
43
49
|
|
|
44
50
|
The package ships a default `sabi.config.json` next to the bundle, so it works with no setup.
|
|
45
51
|
To change tiers, policy or telemetry, put your own `sabi.config.json` in the project you run in,
|
|
46
|
-
or at `~/.config/sabi/sabi.config.json
|
|
52
|
+
or at `~/.config/sabi/sabi.config.json`; both take precedence over the shipped default (or set
|
|
47
53
|
`SABI_CONFIG` to point at one explicitly).
|
|
48
54
|
|
|
49
55
|
`harness.tiers` defaults to the strongest Command Code ids available from the Go plan up. A model
|
package/mod/sabi.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @vizuh/sabi 0.1.
|
|
1
|
+
// @vizuh/sabi 0.1.3 — 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/state.ts
|
|
@@ -589,6 +589,31 @@ function validateConfig(value, source = "<inline>") {
|
|
|
589
589
|
throw new Error(`Sabi config ${source}: telemetry.captureChars must be a positive number`);
|
|
590
590
|
}
|
|
591
591
|
}
|
|
592
|
+
const controller = config.controller;
|
|
593
|
+
if (controller !== void 0) {
|
|
594
|
+
if (!isObject(controller)) throw new Error(`Sabi config ${source}: controller must be an object`);
|
|
595
|
+
const controllerFields = /* @__PURE__ */ new Set(["preferredHarnesses", "harnesses"]);
|
|
596
|
+
for (const field of Object.keys(controller)) {
|
|
597
|
+
if (!controllerFields.has(field)) throw new Error(`Sabi config ${source}: controller.${field} is not a supported field`);
|
|
598
|
+
}
|
|
599
|
+
const validateStrings = (value2, label) => {
|
|
600
|
+
if (!Array.isArray(value2) || value2.some((item) => typeof item !== "string" || !item.trim())) {
|
|
601
|
+
throw new Error(`Sabi config ${source}: ${label} must be an array of nonempty strings`);
|
|
602
|
+
}
|
|
603
|
+
if (new Set(value2).size !== value2.length) throw new Error(`Sabi config ${source}: ${label} must not contain duplicates`);
|
|
604
|
+
};
|
|
605
|
+
if (controller.preferredHarnesses !== void 0) validateStrings(controller.preferredHarnesses, "controller.preferredHarnesses");
|
|
606
|
+
if (controller.harnesses !== void 0) {
|
|
607
|
+
if (!isObject(controller.harnesses)) throw new Error(`Sabi config ${source}: controller.harnesses must be an object`);
|
|
608
|
+
for (const [harnessName, settings] of Object.entries(controller.harnesses)) {
|
|
609
|
+
if (!isObject(settings)) throw new Error(`Sabi config ${source}: controller.harnesses.${harnessName} must be an object`);
|
|
610
|
+
for (const field of Object.keys(settings)) {
|
|
611
|
+
if (field !== "preferredModels") throw new Error(`Sabi config ${source}: controller.harnesses.${harnessName}.${field} is not a supported field`);
|
|
612
|
+
}
|
|
613
|
+
if (settings.preferredModels !== void 0) validateStrings(settings.preferredModels, `controller.harnesses.${harnessName}.preferredModels`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
592
617
|
const harness = config.harness;
|
|
593
618
|
if (harness !== void 0) {
|
|
594
619
|
if (typeof harness !== "object" || harness === null) {
|
|
@@ -612,6 +637,25 @@ function validateConfig(value, source = "<inline>") {
|
|
|
612
637
|
return { ...config, upstreams, models, aliases, policy };
|
|
613
638
|
}
|
|
614
639
|
|
|
640
|
+
// packages/core/src/log.ts
|
|
641
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
642
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
643
|
+
import path2 from "node:path";
|
|
644
|
+
function defaultLogPath() {
|
|
645
|
+
return process.env.SABI_LOG?.trim() || path2.join(process.cwd(), ".sabi", "decisions.jsonl");
|
|
646
|
+
}
|
|
647
|
+
function appendDecision(record, logFile = defaultLogPath()) {
|
|
648
|
+
mkdirSync(path2.dirname(logFile), { recursive: true });
|
|
649
|
+
appendFileSync(logFile, `${JSON.stringify(record)}
|
|
650
|
+
`);
|
|
651
|
+
}
|
|
652
|
+
function hashIdentity(kind, ...parts) {
|
|
653
|
+
return createHash("sha256").update(JSON.stringify([kind, ...parts])).digest("hex");
|
|
654
|
+
}
|
|
655
|
+
function sessionIdFor(session, client = "unknown") {
|
|
656
|
+
return session === void 0 ? randomUUID() : hashIdentity("session", client, session);
|
|
657
|
+
}
|
|
658
|
+
|
|
615
659
|
// packages/core/src/harness.ts
|
|
616
660
|
function roundKindOf(round, calls) {
|
|
617
661
|
if (round.assistantTurns === 0 || round.lastRole === "user") return "first-turn";
|
|
@@ -733,8 +777,10 @@ function sanitizeReason(reason, policy) {
|
|
|
733
777
|
import * as readline from "node:readline/promises";
|
|
734
778
|
|
|
735
779
|
// packages/adapters/command-code/mod/sabi.ts
|
|
780
|
+
import path3 from "node:path";
|
|
736
781
|
var MOD_ID = "sabi";
|
|
737
782
|
var DECISION_TYPE = "sabi/decision";
|
|
783
|
+
var CLIENT_ID = "command-code";
|
|
738
784
|
function readLedger(state) {
|
|
739
785
|
const raw = state.modState?.[MOD_ID] ?? {};
|
|
740
786
|
return {
|
|
@@ -746,7 +792,8 @@ function readLedger(state) {
|
|
|
746
792
|
toolNames: Array.isArray(raw.toolNames) ? raw.toolNames : [],
|
|
747
793
|
hasTools: raw.hasTools === true,
|
|
748
794
|
lastModel: raw.lastModel,
|
|
749
|
-
lastUsage: raw.lastUsage
|
|
795
|
+
lastUsage: raw.lastUsage,
|
|
796
|
+
sessionId: typeof raw.sessionId === "string" ? raw.sessionId : void 0
|
|
750
797
|
};
|
|
751
798
|
}
|
|
752
799
|
function writeLedger(state, ledger) {
|
|
@@ -755,6 +802,29 @@ function writeLedger(state, ledger) {
|
|
|
755
802
|
function compactedSince(ledger, stats) {
|
|
756
803
|
return ledger.messageCount > 0 && stats.messageCount > 0 && stats.messageCount < ledger.messageCount;
|
|
757
804
|
}
|
|
805
|
+
function hashedToolNames(names) {
|
|
806
|
+
return names.map((name) => hashIdentity("tool", name));
|
|
807
|
+
}
|
|
808
|
+
function toUsageTotals(usage) {
|
|
809
|
+
if (!usage) return void 0;
|
|
810
|
+
const validTokens = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
811
|
+
if (!validTokens(usage.inputTokens) || !validTokens(usage.outputTokens)) return void 0;
|
|
812
|
+
const cachedTokens = usage.cachedInputTokens === void 0 ? 0 : usage.cachedInputTokens;
|
|
813
|
+
if (!validTokens(cachedTokens) || cachedTokens > usage.inputTokens) return void 0;
|
|
814
|
+
const promptTokens = usage.inputTokens;
|
|
815
|
+
const completionTokens = usage.outputTokens;
|
|
816
|
+
const totalTokens = promptTokens + completionTokens;
|
|
817
|
+
if (!Number.isSafeInteger(totalTokens)) return void 0;
|
|
818
|
+
return {
|
|
819
|
+
promptTokens,
|
|
820
|
+
completionTokens,
|
|
821
|
+
cachedTokens,
|
|
822
|
+
totalTokens
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function logPathFor(ctx) {
|
|
826
|
+
return ctx?.cwd?.trim() ? path3.join(ctx.cwd, ".sabi", "decisions.jsonl") : void 0;
|
|
827
|
+
}
|
|
758
828
|
function sabi(cmd) {
|
|
759
829
|
let config;
|
|
760
830
|
try {
|
|
@@ -785,6 +855,9 @@ function sabi(cmd) {
|
|
|
785
855
|
nextPlan = void 0;
|
|
786
856
|
servedBy = void 0;
|
|
787
857
|
const ledger = readLedger(state);
|
|
858
|
+
if (!ledger.sessionId) {
|
|
859
|
+
ledger.sessionId = sessionIdFor(void 0, CLIENT_ID);
|
|
860
|
+
}
|
|
788
861
|
return writeLedger(state, { ...ledger, rounds: turnNumber });
|
|
789
862
|
},
|
|
790
863
|
// Sabi observes tool outcomes and never rewrites what the model sees.
|
|
@@ -847,7 +920,7 @@ function sabi(cmd) {
|
|
|
847
920
|
lastUsage: usedThisTurn ? usage : void 0
|
|
848
921
|
};
|
|
849
922
|
previousFailure = adopted ? { failure: adopted.state.failure, failureEvidence: adopted.state.failureEvidence } : void 0;
|
|
850
|
-
|
|
923
|
+
recordCustomEntry(ctx, {
|
|
851
924
|
turn: turnNumber,
|
|
852
925
|
planned: servingPlan ? {
|
|
853
926
|
tier: servingPlan.tier,
|
|
@@ -869,11 +942,49 @@ function sabi(cmd) {
|
|
|
869
942
|
// Decision records never embed raw tool output by default; snippet capture is opt-in.
|
|
870
943
|
captureSnippets: telemetry.captureSnippets
|
|
871
944
|
});
|
|
945
|
+
if (servingPlan) {
|
|
946
|
+
const sessionId = ledger.sessionId ?? sessionIdFor(void 0, CLIENT_ID);
|
|
947
|
+
const usageTotals = toUsageTotals(usage);
|
|
948
|
+
const decision = {
|
|
949
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
950
|
+
sessionId,
|
|
951
|
+
// sessionKnown stays false: the host does not expose a real session ID to the mod.
|
|
952
|
+
client: CLIENT_ID,
|
|
953
|
+
turnId: hashIdentity("turn", sessionId, String(turnNumber)),
|
|
954
|
+
servedModel: servedBy ?? void 0,
|
|
955
|
+
alias: "sabi-code",
|
|
956
|
+
mode: "auto",
|
|
957
|
+
rule: servingPlan.rule,
|
|
958
|
+
tier: servingPlan.tier,
|
|
959
|
+
reason: sanitizeReason(String(servingPlan.reason ?? ""), telemetry),
|
|
960
|
+
// This is the host adapter, not a provider entitlement claim.
|
|
961
|
+
upstream: CLIENT_ID,
|
|
962
|
+
upstreamModel: servingPlan.model,
|
|
963
|
+
stream: false,
|
|
964
|
+
state: {
|
|
965
|
+
...servingPlan.state,
|
|
966
|
+
// Hash tool names; never persist raw tool outputs or args.
|
|
967
|
+
toolNames: hashedToolNames(servingPlan.state.toolNames),
|
|
968
|
+
lastToolNames: hashedToolNames(servingPlan.state.lastToolNames)
|
|
969
|
+
},
|
|
970
|
+
sessionKnown: false,
|
|
971
|
+
...usageTotals ? { usage: usageTotals } : {},
|
|
972
|
+
// outcome means the model request round completed, not that the whole task succeeded.
|
|
973
|
+
outcome: "ok"
|
|
974
|
+
};
|
|
975
|
+
const logFile = logPathFor(ctx);
|
|
976
|
+
if (logFile) {
|
|
977
|
+
try {
|
|
978
|
+
appendDecision(decision, logFile);
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
872
983
|
return writeLedger(state, next);
|
|
873
984
|
}
|
|
874
985
|
});
|
|
875
986
|
}
|
|
876
|
-
function
|
|
987
|
+
function recordCustomEntry(ctx, data) {
|
|
877
988
|
ctx?.session?.appendCustomEntry({ customType: DECISION_TYPE, data });
|
|
878
989
|
}
|
|
879
990
|
export {
|
package/package.json
CHANGED
package/sabi.config.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"provenance": "Model ids, context windows and prices verified live from https://openrouter.ai/api/v1/models on 2026-09-
|
|
2
|
+
"provenance": "Model ids, context windows, output limits and prices verified live from https://openrouter.ai/api/v1/models on 2026-09-20; input modalities for the same ids verified from the same endpoint. Prices are USD per 1M tokens. Declared modalities are enforced: an undeclared capability is unknown, a declared one is binding.",
|
|
3
3
|
"server": { "host": "127.0.0.1", "port": 8787 },
|
|
4
4
|
"upstreams": {
|
|
5
5
|
"openrouter": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"upstream": "openrouter",
|
|
23
23
|
"model": "deepseek/deepseek-v4-flash-0731",
|
|
24
24
|
"contextWindow": 1310720,
|
|
25
|
+
"maxOutputTokens": 943718,
|
|
25
26
|
"capabilities": { "inputModalities": ["text"] },
|
|
26
27
|
"cost": { "input": 0.06, "output": 0.12, "cacheRead": 0.012 }
|
|
27
28
|
},
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
"upstream": "openrouter",
|
|
30
31
|
"model": "openai/gpt-5.6-luna",
|
|
31
32
|
"contextWindow": 1050000,
|
|
33
|
+
"maxOutputTokens": 128000,
|
|
32
34
|
"capabilities": { "inputModalities": ["text", "image", "file"] },
|
|
33
35
|
"cost": { "input": 0.2, "output": 1.2, "cacheRead": 0.02 }
|
|
34
36
|
},
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"upstream": "openrouter",
|
|
37
39
|
"model": "anthropic/claude-sonnet-5",
|
|
38
40
|
"contextWindow": 1000000,
|
|
41
|
+
"maxOutputTokens": 128000,
|
|
39
42
|
"capabilities": { "inputModalities": ["text", "image", "file"] },
|
|
40
43
|
"cost": { "input": 2, "output": 10, "cacheRead": 0.2 }
|
|
41
44
|
},
|
|
@@ -70,6 +73,13 @@
|
|
|
70
73
|
"captureSnippets": false,
|
|
71
74
|
"captureChars": 800
|
|
72
75
|
},
|
|
76
|
+
"controller": {
|
|
77
|
+
"preferredHarnesses": ["opencode", "command-code", "claude", "codex", "hermes"],
|
|
78
|
+
"harnesses": {
|
|
79
|
+
"command-code": { "preferredModels": ["moonshotai/kimi-k3"] },
|
|
80
|
+
"opencode": { "preferredModels": ["opencode-go/kimi-k3"] }
|
|
81
|
+
}
|
|
82
|
+
},
|
|
73
83
|
"judge": {
|
|
74
84
|
"enabled": true,
|
|
75
85
|
"baseURL": "https://api.typesafe.ai/v1",
|